From 03550dc4435c7668d36b50ca5ae420fab94e4936 Mon Sep 17 00:00:00 2001 From: Thomas Vaillant Date: Fri, 25 Sep 2020 18:34:23 +0200 Subject: [PATCH] feat: initial features --- .editorconfig | 11 + .eslintrc.js | 85 + .github/ISSUE_TEMPLATE/bug.md | 43 + .github/ISSUE_TEMPLATE/config.yml | 7 + .github/ISSUE_TEMPLATE/feature.md | 22 + .github/workflows/build.yml | 230 + .github/workflows/ci.yml | 133 + .gitignore | 3 + .log4brains.yml | 14 + .prettierignore | 5 + .vscode/settings.json | 46 + CODE_OF_CONDUCT.md | 132 + CONTRIBUTING.md | 60 + LICENSE | 2 +- README.md | 515 +- commitlint.config.js | 1 + docs/Log4brains-logo-full.png | Bin 0 -> 31547 bytes ...markdown-architectural-decision-records.md | 37 + ...cture-in-a-monorepo-with-yarn-and-lerna.md | 40 + ...ettier-eslint-airbnb-for-the-code-style.md | 33 + ...926-use-the-adr-number-as-its-unique-id.md | 25 + docs/adr/20200927-avoid-default-exports.md | 13 + ...01016-use-the-adr-slug-as-its-unique-id.md | 30 + ...nhancing-the-adr-markdown-body-with-mdx.md | 33 + docs/adr/20201103-use-lunr-for-search.md | 47 + docs/adr/README.md | 33 + docs/adr/index.md | 36 + docs/adr/template.md | 73 + docs/demo.gif | Bin 0 -> 435446 bytes e2e-tests/e2e-launcher.js | 77 + jest.config.base.js | 4 + lerna.json | 14 + package.json | 82 + packages/cli-common/.eslintrc.js | 11 + packages/cli-common/README.md | 18 + packages/cli-common/dev-tests/run.ts | 137 + packages/cli-common/nodemon.json | 5 + packages/cli-common/package.json | 52 + packages/cli-common/src/AppConsole.ts | 212 + packages/cli-common/src/ConsoleCapturer.ts | 82 + packages/cli-common/src/index.ts | 2 + packages/cli-common/tsconfig.build.json | 4 + packages/cli-common/tsconfig.json | 15 + packages/cli/.eslintrc.js | 11 + packages/cli/README.md | 49 + packages/cli/nodemon.json | 5 + packages/cli/package.json | 50 + packages/cli/src/cli.ts | 73 + packages/cli/src/commands/ListCommand.ts | 43 + packages/cli/src/commands/NewCommand.ts | 172 + packages/cli/src/commands/index.ts | 2 + packages/cli/src/log4brains | 5 + packages/cli/src/main.ts | 55 + packages/cli/src/utils.ts | 15 + packages/cli/tsconfig.build.json | 4 + packages/cli/tsconfig.json | 16 + packages/core/.eslintrc.js | 11 + packages/core/README.md | 36 + packages/core/docs/.gitignore | 1 + ...t-architecture-and-ddd-for-the-core-api.md | 9 + ...-markdown-parsing-is-part-of-the-domain.md | 26 + ...0201027-adr-link-resolver-in-the-domain.md | 21 + .../__snapshots__/ro.test.ts.snap | 976 + .../__snapshots__/rw.test.ts.snap | 79 + .../ro-project/.log4brains.yml | 13 + .../ro-project/docs/adr/20200101-first-adr.md | 16 + .../docs/adr/20200102-adr-only-with-date.md | 11 + .../docs/adr/20200102-adr-with-intro.md | 18 + .../docs/adr/20200102-adr-without-status.md | 13 + .../docs/adr/20200102-adr-without-title.md | 13 + .../20201028-adr-with-no-metadata-no-title.md | 7 + .../docs/adr/20201028-adr-with-no-metadata.md | 9 + .../docs/adr/20201028-adr-without-date.md | 15 + .../ro-project/docs/adr/20201028-links.md | 12 + .../docs/adr/20201028-superseded-adr.md | 12 + .../docs/adr/20201029-proposed-adr.md | 12 + .../docs/adr/20201029-rejected-adr.md | 12 + .../docs/adr/20201029-superseder.md | 16 + .../ro-project/docs/adr/20201030-draft-adr.md | 12 + .../docs/adr/adr_with_a_WeIrd-filename.md | 12 + .../ro-project/docs/adr/template.md | 3 + .../package1/adr/20200101-first-adr.md | 16 + .../package1/adr/20201028-adr-without-date.md | 15 + .../package1/adr/20201028-links-in-package.md | 13 + .../package1/adr/20201028-links-to-global.md | 11 + .../packages/package1/adr/template.md | 3 + .../adr/20201028-links-to-another-package.md | 8 + packages/core/integration-tests/ro.test.ts | 92 + .../integration-tests/rw-project/.gitignore | 2 + .../rw-project/.log4brains.yml | 13 + .../rw-project/docs/adr/template.md | 25 + .../packages/package1/adr/.gitignore | 0 .../packages/package1/adr/template.md | 9 + .../packages/package2/adr/.gitignore | 0 packages/core/integration-tests/rw.test.ts | 112 + packages/core/jest.config.js | 14 + packages/core/package.json | 70 + .../CreateAdrFromTemplateCommandHandler.ts | 31 + .../SupersedeAdrCommandHandler.ts | 25 + .../adr/application/command-handlers/index.ts | 2 + .../commands/CreateAdrFromTemplateCommand.ts | 8 + .../commands/SupersedeAdrCommand.ts | 11 + .../src/adr/application/commands/index.ts | 2 + packages/core/src/adr/application/index.ts | 5 + .../GenerateAdrSlugFromTitleCommand.ts | 11 + .../application/queries/GetAdrBySlugQuery.ts | 8 + .../application/queries/SearchAdrsQuery.ts | 12 + .../core/src/adr/application/queries/index.ts | 3 + .../GenerateAdrSlugFromTitleCommandHandler.ts | 24 + .../GetAdrBySlugQueryHandler.ts | 32 + .../SearchAdrsQueryHandler.test.ts | 53 + .../query-handlers/SearchAdrsQueryHandler.ts | 31 + .../adr/application/query-handlers/index.ts | 3 + .../application/repositories/AdrRepository.ts | 8 + .../repositories/AdrTemplateRepository.ts | 5 + .../src/adr/application/repositories/index.ts | 2 + packages/core/src/adr/domain/Adr.test.ts | 440 + packages/core/src/adr/domain/Adr.ts | 231 + packages/core/src/adr/domain/AdrFile.test.ts | 35 + packages/core/src/adr/domain/AdrFile.ts | 52 + .../core/src/adr/domain/AdrRelation.test.ts | 42 + packages/core/src/adr/domain/AdrRelation.ts | 32 + packages/core/src/adr/domain/AdrSlug.test.ts | 65 + packages/core/src/adr/domain/AdrSlug.ts | 58 + .../core/src/adr/domain/AdrStatus.test.ts | 19 + packages/core/src/adr/domain/AdrStatus.ts | 45 + .../core/src/adr/domain/AdrTemplate.test.ts | 53 + packages/core/src/adr/domain/AdrTemplate.ts | 42 + packages/core/src/adr/domain/Author.ts | 24 + .../src/adr/domain/FilesystemPath.test.ts | 74 + .../core/src/adr/domain/FilesystemPath.ts | 77 + .../src/adr/domain/MarkdownAdrLink.test.ts | 36 + .../core/src/adr/domain/MarkdownAdrLink.ts | 32 + .../src/adr/domain/MarkdownAdrLinkResolver.ts | 6 + .../core/src/adr/domain/MarkdownBody.test.ts | 297 + packages/core/src/adr/domain/MarkdownBody.ts | 248 + packages/core/src/adr/domain/Package.ts | 23 + packages/core/src/adr/domain/PackageRef.ts | 15 + packages/core/src/adr/domain/index.ts | 13 + .../infrastructure/MarkdownAdrLinkResolver.ts | 40 + .../repositories/AdrRepository.ts | 301 + .../repositories/AdrTemplateRepository.ts | 70 + .../repositories/PackageRepository.ts | 87 + .../adr/infrastructure/repositories/index.ts | 3 + packages/core/src/application/Command.ts | 1 + .../core/src/application/CommandHandler.ts | 8 + packages/core/src/application/Query.ts | 1 + packages/core/src/application/QueryHandler.ts | 8 + packages/core/src/application/index.ts | 4 + packages/core/src/decs.d.ts | 12 + packages/core/src/domain/AggregateRoot.ts | 5 + packages/core/src/domain/Entity.ts | 7 + packages/core/src/domain/Log4brainsError.ts | 9 + packages/core/src/domain/ValueObject.test.ts | 49 + packages/core/src/domain/ValueObject.ts | 31 + packages/core/src/domain/ValueObjectArray.ts | 11 + packages/core/src/domain/ValueObjectMap.ts | 86 + packages/core/src/domain/index.ts | 6 + packages/core/src/index.ts | 6 + .../core/src/infrastructure/api/Log4brains.ts | 214 + packages/core/src/infrastructure/api/index.ts | 2 + .../api/transformers/adr-transformers.ts | 60 + .../infrastructure/api/transformers/index.ts | 1 + .../src/infrastructure/api/types/AdrDto.ts | 37 + .../src/infrastructure/api/types/index.ts | 1 + .../src/infrastructure/buses/CommandBus.ts | 22 + .../core/src/infrastructure/buses/QueryBus.ts | 22 + .../core/src/infrastructure/buses/index.ts | 2 + .../src/infrastructure/config/builders.ts | 85 + .../config/guessGitRepositoryConfig.ts | 84 + .../core/src/infrastructure/config/index.ts | 2 + .../core/src/infrastructure/config/schema.ts | 58 + .../src/infrastructure/di/buildContainer.ts | 89 + packages/core/src/infrastructure/di/index.ts | 1 + .../file-watcher/FileWatcher.ts | 83 + .../src/infrastructure/file-watcher/index.ts | 1 + .../lib/cheerio-markdown/CheerioMarkdown.ts | 141 + .../CheerioMarkdownElement.ts | 25 + .../lib/cheerio-markdown/cheerioToMarkdown.ts | 22 + .../core/src/lib/cheerio-markdown/index.ts | 2 + .../markdown-it-source-map-plugin.ts | 27 + packages/core/src/lib/paths.ts | 3 + packages/core/src/polyfills.ts | 1 + packages/core/src/utils.ts | 23 + packages/core/tsconfig.build.json | 4 + packages/core/tsconfig.json | 16 + packages/init/.eslintrc.js | 11 + packages/init/README.md | 24 + packages/init/assets/README.md | 33 + packages/init/assets/index.md | 36 + packages/init/assets/template.md | 73 + .../use-log4brains-to-manage-the-adrs.md | 21 + ...markdown-architectural-decision-records.md | 41 + packages/init/integration-tests/init.test.ts | 203 + packages/init/jest.config.js | 13 + packages/init/nodemon.json | 5 + packages/init/package.json | 67 + packages/init/src/cli.ts | 32 + packages/init/src/commands/FailureExit.ts | 5 + packages/init/src/commands/InitCommand.ts | 483 + packages/init/src/commands/index.ts | 2 + packages/init/src/log4brains-init | 5 + packages/init/src/main.ts | 29 + packages/init/tsconfig.build.json | 4 + packages/init/tsconfig.json | 15 + packages/web/.babelrc | 24 + packages/web/.eslintrc.js | 23 + packages/web/.gitignore | 34 + packages/web/.storybook/main.js | 22 + packages/web/.storybook/mocks/adrs.ts | 204 + packages/web/.storybook/mocks/index.ts | 1 + packages/web/.storybook/preview-head.html | 9 + packages/web/.storybook/preview.js | 21 + packages/web/README.md | 73 + ...5-use-nextjs-for-static-site-generation.md | 138 + ...act-file-structure-organized-by-feature.md | 8 + .../docs/adr/20200927-avoid-react-fc-type.md | 46 + .../web/docs/adr/20200927-use-react-hooks.md | 12 + ...01007-next-js-persistent-layout-pattern.md | 12 + packages/web/jest.config.js | 11 + packages/web/next-env.d.ts | 2 + packages/web/next.config.js | 44 + packages/web/nodemon.json | 5 + packages/web/package.json | 94 + packages/web/public/favicon.ico | Bin 0 -> 15086 bytes .../l4b-static/Log4brains-logo-dark.png | Bin 0 -> 22071 bytes .../web/public/l4b-static/Log4brains-logo.png | Bin 0 -> 62517 bytes .../web/public/l4b-static/Log4brains-og.png | Bin 0 -> 41376 bytes .../web/public/l4b-static/adr-workflow.png | Bin 0 -> 10768 bytes packages/web/src/bin/log4brains-web | 3 + packages/web/src/bin/main.ts | 58 + packages/web/src/cli/build.ts | 117 + packages/web/src/cli/index.ts | 2 + packages/web/src/cli/preview.ts | 118 + .../AdrStatusChip/AdrStatusChip.stories.tsx | 30 + .../AdrStatusChip/AdrStatusChip.tsx | 63 + .../web/src/components/AdrStatusChip/index.ts | 1 + .../components/Markdown/Markdown.stories.tsx | 141 + .../web/src/components/Markdown/Markdown.tsx | 116 + .../Markdown/components/AdrLink/AdrLink.tsx | 49 + .../Markdown/components/AdrLink/index.ts | 1 + .../components/Markdown/components/index.ts | 1 + packages/web/src/components/Markdown/hljs.css | 3 + packages/web/src/components/Markdown/index.ts | 1 + .../MarkdownHeading/MarkdownHeading.tsx | 78 + .../src/components/MarkdownHeading/index.ts | 1 + .../MarkdownToc/MarkdownToc.stories.tsx | 58 + .../MarkdownToc/MarkdownToc.test.tsx | 58 + .../components/MarkdownToc/MarkdownToc.tsx | 110 + .../__snapshots__/MarkdownToc.test.tsx.snap | 125 + .../web/src/components/MarkdownToc/index.ts | 1 + .../SearchBox/SearchBox.stories.tsx | 56 + .../src/components/SearchBox/SearchBox.tsx | 196 + .../components/SearchBar/SearchBar.tsx | 89 + .../SearchBox/components/SearchBar/index.ts | 1 + .../web/src/components/SearchBox/index.ts | 1 + .../TwoColContent/TwoColContent.stories.tsx | 117 + .../TwoColContent/TwoColContent.tsx | 72 + .../web/src/components/TwoColContent/index.ts | 1 + packages/web/src/components/index.ts | 6 + .../contexts/AdrNavContext/AdrNavContext.ts | 9 + .../web/src/contexts/AdrNavContext/index.ts | 1 + .../Log4brainsModeContext.ts | 8 + .../contexts/Log4brainsModeContext/index.ts | 1 + packages/web/src/contexts/index.ts | 2 + .../AdrBrowserLayout.stories.tsx | 29 + .../AdrBrowserLayout/AdrBrowserLayout.tsx | 485 + .../ConnectedAdrBrowserLayout.tsx | 184 + .../components/AdrMenu/AdrMenu.tsx | 214 + .../components/AdrMenu/index.ts | 1 + .../ConnectedSearchBox/ConnectedSearchBox.tsx | 65 + .../components/ConnectedSearchBox/index.ts | 1 + .../RoutingProgress/RoutingProgress.tsx | 44 + .../components/RoutingProgress/index.ts | 1 + .../AdrBrowserLayout/components/index.ts | 2 + .../web/src/layouts/AdrBrowserLayout/index.ts | 3 + packages/web/src/layouts/index.ts | 2 + packages/web/src/lib/adr-utils.ts | 12 + packages/web/src/lib/console.ts | 27 + .../src/lib/core-api/getIndexPageMarkdown.ts | 23 + packages/web/src/lib/core-api/index.ts | 2 + packages/web/src/lib/core-api/instance.ts | 22 + .../web/src/lib/core-api/noop/.log4brains.yml | 7 + .../lib/core-api/noop/noop-adrs/.gitignore | 0 packages/web/src/lib/debug.ts | 7 + packages/web/src/lib/next.ts | 40 + packages/web/src/lib/search/Search.ts | 100 + packages/web/src/lib/search/index.ts | 2 + packages/web/src/lib/search/instance.ts | 29 + packages/web/src/lib/slugify.ts | 9 + packages/web/src/lib/toc-utils/Toc.ts | 23 + .../web/src/lib/toc-utils/TocBuilder.test.ts | 121 + packages/web/src/lib/toc-utils/TocBuilder.ts | 40 + .../web/src/lib/toc-utils/TocContainer.ts | 5 + packages/web/src/lib/toc-utils/TocSection.ts | 34 + packages/web/src/lib/toc-utils/index.ts | 4 + packages/web/src/mui/MuiDecorator.tsx | 17 + packages/web/src/mui/index.ts | 2 + packages/web/src/mui/theme.ts | 110 + packages/web/src/pages/_app.tsx | 62 + packages/web/src/pages/_document.tsx | 83 + packages/web/src/pages/adr/[...slug].tsx | 49 + packages/web/src/pages/api/adr.ts | 17 + .../web/src/pages/api/adr/[...slugAndMore].ts | 62 + packages/web/src/pages/api/search-index.ts | 21 + packages/web/src/pages/index.tsx | 19 + .../src/scenes/AdrScene/AdrScene.stories.tsx | 55 + packages/web/src/scenes/AdrScene/AdrScene.tsx | 184 + .../components/AdrHeader/AdrHeader.test.tsx | 284 + .../components/AdrHeader/AdrHeader.tsx | 255 + .../__snapshots__/AdrHeader.test.tsx.snap | 1572 ++ .../AdrScene/components/AdrHeader/index.ts | 1 + .../src/scenes/AdrScene/components/index.ts | 1 + packages/web/src/scenes/AdrScene/index.ts | 2 + .../scenes/IndexScene/IndexScene.stories.tsx | 42 + .../web/src/scenes/IndexScene/IndexScene.tsx | 65 + packages/web/src/scenes/IndexScene/index.ts | 2 + packages/web/src/scenes/index.ts | 3 + packages/web/src/types.ts | 41 + packages/web/tsconfig.dev.json | 4 + packages/web/tsconfig.json | 18 + tsconfig.json | 13 + yarn.lock | 20118 ++++++++++++++++ 323 files changed, 36918 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc.js create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.md create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .log4brains.yml create mode 100644 .prettierignore create mode 100644 .vscode/settings.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 commitlint.config.js create mode 100644 docs/Log4brains-logo-full.png create mode 100644 docs/adr/20200924-use-markdown-architectural-decision-records.md create mode 100644 docs/adr/20200925-multi-packages-architecture-in-a-monorepo-with-yarn-and-lerna.md create mode 100644 docs/adr/20200925-use-prettier-eslint-airbnb-for-the-code-style.md create mode 100644 docs/adr/20200926-use-the-adr-number-as-its-unique-id.md create mode 100644 docs/adr/20200927-avoid-default-exports.md create mode 100644 docs/adr/20201016-use-the-adr-slug-as-its-unique-id.md create mode 100644 docs/adr/20201026-the-core-api-is-responsible-for-enhancing-the-adr-markdown-body-with-mdx.md create mode 100644 docs/adr/20201103-use-lunr-for-search.md create mode 100644 docs/adr/README.md create mode 100644 docs/adr/index.md create mode 100644 docs/adr/template.md create mode 100644 docs/demo.gif create mode 100644 e2e-tests/e2e-launcher.js create mode 100644 jest.config.base.js create mode 100644 lerna.json create mode 100644 package.json create mode 100644 packages/cli-common/.eslintrc.js create mode 100644 packages/cli-common/README.md create mode 100644 packages/cli-common/dev-tests/run.ts create mode 100644 packages/cli-common/nodemon.json create mode 100644 packages/cli-common/package.json create mode 100644 packages/cli-common/src/AppConsole.ts create mode 100644 packages/cli-common/src/ConsoleCapturer.ts create mode 100644 packages/cli-common/src/index.ts create mode 100644 packages/cli-common/tsconfig.build.json create mode 100644 packages/cli-common/tsconfig.json create mode 100644 packages/cli/.eslintrc.js create mode 100644 packages/cli/README.md create mode 100644 packages/cli/nodemon.json create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/cli.ts create mode 100644 packages/cli/src/commands/ListCommand.ts create mode 100644 packages/cli/src/commands/NewCommand.ts create mode 100644 packages/cli/src/commands/index.ts create mode 100755 packages/cli/src/log4brains create mode 100644 packages/cli/src/main.ts create mode 100644 packages/cli/src/utils.ts create mode 100644 packages/cli/tsconfig.build.json create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/core/.eslintrc.js create mode 100644 packages/core/README.md create mode 100644 packages/core/docs/.gitignore create mode 100644 packages/core/docs/adr/20201002-use-explicit-architecture-and-ddd-for-the-core-api.md create mode 100644 packages/core/docs/adr/20201003-markdown-parsing-is-part-of-the-domain.md create mode 100644 packages/core/docs/adr/20201027-adr-link-resolver-in-the-domain.md create mode 100644 packages/core/integration-tests/__snapshots__/ro.test.ts.snap create mode 100644 packages/core/integration-tests/__snapshots__/rw.test.ts.snap create mode 100644 packages/core/integration-tests/ro-project/.log4brains.yml create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20200101-first-adr.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20200102-adr-only-with-date.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20200102-adr-with-intro.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-status.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-title.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata-no-title.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201028-adr-without-date.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201028-links.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201028-superseded-adr.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201029-proposed-adr.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201029-rejected-adr.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201029-superseder.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/20201030-draft-adr.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/adr_with_a_WeIrd-filename.md create mode 100644 packages/core/integration-tests/ro-project/docs/adr/template.md create mode 100644 packages/core/integration-tests/ro-project/packages/package1/adr/20200101-first-adr.md create mode 100644 packages/core/integration-tests/ro-project/packages/package1/adr/20201028-adr-without-date.md create mode 100644 packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-in-package.md create mode 100644 packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-to-global.md create mode 100644 packages/core/integration-tests/ro-project/packages/package1/adr/template.md create mode 100644 packages/core/integration-tests/ro-project/packages/package2/adr/20201028-links-to-another-package.md create mode 100644 packages/core/integration-tests/ro.test.ts create mode 100644 packages/core/integration-tests/rw-project/.gitignore create mode 100644 packages/core/integration-tests/rw-project/.log4brains.yml create mode 100644 packages/core/integration-tests/rw-project/docs/adr/template.md create mode 100644 packages/core/integration-tests/rw-project/packages/package1/adr/.gitignore create mode 100644 packages/core/integration-tests/rw-project/packages/package1/adr/template.md create mode 100644 packages/core/integration-tests/rw-project/packages/package2/adr/.gitignore create mode 100644 packages/core/integration-tests/rw.test.ts create mode 100644 packages/core/jest.config.js create mode 100644 packages/core/package.json create mode 100644 packages/core/src/adr/application/command-handlers/CreateAdrFromTemplateCommandHandler.ts create mode 100644 packages/core/src/adr/application/command-handlers/SupersedeAdrCommandHandler.ts create mode 100644 packages/core/src/adr/application/command-handlers/index.ts create mode 100644 packages/core/src/adr/application/commands/CreateAdrFromTemplateCommand.ts create mode 100644 packages/core/src/adr/application/commands/SupersedeAdrCommand.ts create mode 100644 packages/core/src/adr/application/commands/index.ts create mode 100644 packages/core/src/adr/application/index.ts create mode 100644 packages/core/src/adr/application/queries/GenerateAdrSlugFromTitleCommand.ts create mode 100644 packages/core/src/adr/application/queries/GetAdrBySlugQuery.ts create mode 100644 packages/core/src/adr/application/queries/SearchAdrsQuery.ts create mode 100644 packages/core/src/adr/application/queries/index.ts create mode 100644 packages/core/src/adr/application/query-handlers/GenerateAdrSlugFromTitleCommandHandler.ts create mode 100644 packages/core/src/adr/application/query-handlers/GetAdrBySlugQueryHandler.ts create mode 100644 packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.test.ts create mode 100644 packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.ts create mode 100644 packages/core/src/adr/application/query-handlers/index.ts create mode 100644 packages/core/src/adr/application/repositories/AdrRepository.ts create mode 100644 packages/core/src/adr/application/repositories/AdrTemplateRepository.ts create mode 100644 packages/core/src/adr/application/repositories/index.ts create mode 100644 packages/core/src/adr/domain/Adr.test.ts create mode 100644 packages/core/src/adr/domain/Adr.ts create mode 100644 packages/core/src/adr/domain/AdrFile.test.ts create mode 100644 packages/core/src/adr/domain/AdrFile.ts create mode 100644 packages/core/src/adr/domain/AdrRelation.test.ts create mode 100644 packages/core/src/adr/domain/AdrRelation.ts create mode 100644 packages/core/src/adr/domain/AdrSlug.test.ts create mode 100644 packages/core/src/adr/domain/AdrSlug.ts create mode 100644 packages/core/src/adr/domain/AdrStatus.test.ts create mode 100644 packages/core/src/adr/domain/AdrStatus.ts create mode 100644 packages/core/src/adr/domain/AdrTemplate.test.ts create mode 100644 packages/core/src/adr/domain/AdrTemplate.ts create mode 100644 packages/core/src/adr/domain/Author.ts create mode 100644 packages/core/src/adr/domain/FilesystemPath.test.ts create mode 100644 packages/core/src/adr/domain/FilesystemPath.ts create mode 100644 packages/core/src/adr/domain/MarkdownAdrLink.test.ts create mode 100644 packages/core/src/adr/domain/MarkdownAdrLink.ts create mode 100644 packages/core/src/adr/domain/MarkdownAdrLinkResolver.ts create mode 100644 packages/core/src/adr/domain/MarkdownBody.test.ts create mode 100644 packages/core/src/adr/domain/MarkdownBody.ts create mode 100644 packages/core/src/adr/domain/Package.ts create mode 100644 packages/core/src/adr/domain/PackageRef.ts create mode 100644 packages/core/src/adr/domain/index.ts create mode 100644 packages/core/src/adr/infrastructure/MarkdownAdrLinkResolver.ts create mode 100644 packages/core/src/adr/infrastructure/repositories/AdrRepository.ts create mode 100644 packages/core/src/adr/infrastructure/repositories/AdrTemplateRepository.ts create mode 100644 packages/core/src/adr/infrastructure/repositories/PackageRepository.ts create mode 100644 packages/core/src/adr/infrastructure/repositories/index.ts create mode 100644 packages/core/src/application/Command.ts create mode 100644 packages/core/src/application/CommandHandler.ts create mode 100644 packages/core/src/application/Query.ts create mode 100644 packages/core/src/application/QueryHandler.ts create mode 100644 packages/core/src/application/index.ts create mode 100644 packages/core/src/decs.d.ts create mode 100644 packages/core/src/domain/AggregateRoot.ts create mode 100644 packages/core/src/domain/Entity.ts create mode 100644 packages/core/src/domain/Log4brainsError.ts create mode 100644 packages/core/src/domain/ValueObject.test.ts create mode 100644 packages/core/src/domain/ValueObject.ts create mode 100644 packages/core/src/domain/ValueObjectArray.ts create mode 100644 packages/core/src/domain/ValueObjectMap.ts create mode 100644 packages/core/src/domain/index.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/infrastructure/api/Log4brains.ts create mode 100644 packages/core/src/infrastructure/api/index.ts create mode 100644 packages/core/src/infrastructure/api/transformers/adr-transformers.ts create mode 100644 packages/core/src/infrastructure/api/transformers/index.ts create mode 100644 packages/core/src/infrastructure/api/types/AdrDto.ts create mode 100644 packages/core/src/infrastructure/api/types/index.ts create mode 100644 packages/core/src/infrastructure/buses/CommandBus.ts create mode 100644 packages/core/src/infrastructure/buses/QueryBus.ts create mode 100644 packages/core/src/infrastructure/buses/index.ts create mode 100644 packages/core/src/infrastructure/config/builders.ts create mode 100644 packages/core/src/infrastructure/config/guessGitRepositoryConfig.ts create mode 100644 packages/core/src/infrastructure/config/index.ts create mode 100644 packages/core/src/infrastructure/config/schema.ts create mode 100644 packages/core/src/infrastructure/di/buildContainer.ts create mode 100644 packages/core/src/infrastructure/di/index.ts create mode 100644 packages/core/src/infrastructure/file-watcher/FileWatcher.ts create mode 100644 packages/core/src/infrastructure/file-watcher/index.ts create mode 100644 packages/core/src/lib/cheerio-markdown/CheerioMarkdown.ts create mode 100644 packages/core/src/lib/cheerio-markdown/CheerioMarkdownElement.ts create mode 100644 packages/core/src/lib/cheerio-markdown/cheerioToMarkdown.ts create mode 100644 packages/core/src/lib/cheerio-markdown/index.ts create mode 100644 packages/core/src/lib/cheerio-markdown/markdown-it-source-map-plugin.ts create mode 100644 packages/core/src/lib/paths.ts create mode 100644 packages/core/src/polyfills.ts create mode 100644 packages/core/src/utils.ts create mode 100644 packages/core/tsconfig.build.json create mode 100644 packages/core/tsconfig.json create mode 100644 packages/init/.eslintrc.js create mode 100644 packages/init/README.md create mode 100644 packages/init/assets/README.md create mode 100644 packages/init/assets/index.md create mode 100644 packages/init/assets/template.md create mode 100644 packages/init/assets/use-log4brains-to-manage-the-adrs.md create mode 100644 packages/init/assets/use-markdown-architectural-decision-records.md create mode 100644 packages/init/integration-tests/init.test.ts create mode 100644 packages/init/jest.config.js create mode 100644 packages/init/nodemon.json create mode 100644 packages/init/package.json create mode 100644 packages/init/src/cli.ts create mode 100644 packages/init/src/commands/FailureExit.ts create mode 100644 packages/init/src/commands/InitCommand.ts create mode 100644 packages/init/src/commands/index.ts create mode 100755 packages/init/src/log4brains-init create mode 100644 packages/init/src/main.ts create mode 100644 packages/init/tsconfig.build.json create mode 100644 packages/init/tsconfig.json create mode 100644 packages/web/.babelrc create mode 100644 packages/web/.eslintrc.js create mode 100644 packages/web/.gitignore create mode 100644 packages/web/.storybook/main.js create mode 100644 packages/web/.storybook/mocks/adrs.ts create mode 100644 packages/web/.storybook/mocks/index.ts create mode 100644 packages/web/.storybook/preview-head.html create mode 100644 packages/web/.storybook/preview.js create mode 100644 packages/web/README.md create mode 100644 packages/web/docs/adr/20200925-use-nextjs-for-static-site-generation.md create mode 100644 packages/web/docs/adr/20200926-react-file-structure-organized-by-feature.md create mode 100644 packages/web/docs/adr/20200927-avoid-react-fc-type.md create mode 100644 packages/web/docs/adr/20200927-use-react-hooks.md create mode 100644 packages/web/docs/adr/20201007-next-js-persistent-layout-pattern.md create mode 100644 packages/web/jest.config.js create mode 100644 packages/web/next-env.d.ts create mode 100644 packages/web/next.config.js create mode 100644 packages/web/nodemon.json create mode 100644 packages/web/package.json create mode 100644 packages/web/public/favicon.ico create mode 100644 packages/web/public/l4b-static/Log4brains-logo-dark.png create mode 100644 packages/web/public/l4b-static/Log4brains-logo.png create mode 100644 packages/web/public/l4b-static/Log4brains-og.png create mode 100644 packages/web/public/l4b-static/adr-workflow.png create mode 100755 packages/web/src/bin/log4brains-web create mode 100644 packages/web/src/bin/main.ts create mode 100644 packages/web/src/cli/build.ts create mode 100644 packages/web/src/cli/index.ts create mode 100644 packages/web/src/cli/preview.ts create mode 100644 packages/web/src/components/AdrStatusChip/AdrStatusChip.stories.tsx create mode 100644 packages/web/src/components/AdrStatusChip/AdrStatusChip.tsx create mode 100644 packages/web/src/components/AdrStatusChip/index.ts create mode 100644 packages/web/src/components/Markdown/Markdown.stories.tsx create mode 100644 packages/web/src/components/Markdown/Markdown.tsx create mode 100644 packages/web/src/components/Markdown/components/AdrLink/AdrLink.tsx create mode 100644 packages/web/src/components/Markdown/components/AdrLink/index.ts create mode 100644 packages/web/src/components/Markdown/components/index.ts create mode 100644 packages/web/src/components/Markdown/hljs.css create mode 100644 packages/web/src/components/Markdown/index.ts create mode 100644 packages/web/src/components/MarkdownHeading/MarkdownHeading.tsx create mode 100644 packages/web/src/components/MarkdownHeading/index.ts create mode 100644 packages/web/src/components/MarkdownToc/MarkdownToc.stories.tsx create mode 100644 packages/web/src/components/MarkdownToc/MarkdownToc.test.tsx create mode 100644 packages/web/src/components/MarkdownToc/MarkdownToc.tsx create mode 100644 packages/web/src/components/MarkdownToc/__snapshots__/MarkdownToc.test.tsx.snap create mode 100644 packages/web/src/components/MarkdownToc/index.ts create mode 100644 packages/web/src/components/SearchBox/SearchBox.stories.tsx create mode 100644 packages/web/src/components/SearchBox/SearchBox.tsx create mode 100644 packages/web/src/components/SearchBox/components/SearchBar/SearchBar.tsx create mode 100644 packages/web/src/components/SearchBox/components/SearchBar/index.ts create mode 100644 packages/web/src/components/SearchBox/index.ts create mode 100644 packages/web/src/components/TwoColContent/TwoColContent.stories.tsx create mode 100644 packages/web/src/components/TwoColContent/TwoColContent.tsx create mode 100644 packages/web/src/components/TwoColContent/index.ts create mode 100644 packages/web/src/components/index.ts create mode 100644 packages/web/src/contexts/AdrNavContext/AdrNavContext.ts create mode 100644 packages/web/src/contexts/AdrNavContext/index.ts create mode 100644 packages/web/src/contexts/Log4brainsModeContext/Log4brainsModeContext.ts create mode 100644 packages/web/src/contexts/Log4brainsModeContext/index.ts create mode 100644 packages/web/src/contexts/index.ts create mode 100644 packages/web/src/layouts/AdrBrowserLayout/AdrBrowserLayout.stories.tsx create mode 100644 packages/web/src/layouts/AdrBrowserLayout/AdrBrowserLayout.tsx create mode 100644 packages/web/src/layouts/AdrBrowserLayout/ConnectedAdrBrowserLayout.tsx create mode 100644 packages/web/src/layouts/AdrBrowserLayout/components/AdrMenu/AdrMenu.tsx create mode 100644 packages/web/src/layouts/AdrBrowserLayout/components/AdrMenu/index.ts create mode 100644 packages/web/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/ConnectedSearchBox.tsx create mode 100644 packages/web/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/index.ts create mode 100644 packages/web/src/layouts/AdrBrowserLayout/components/RoutingProgress/RoutingProgress.tsx create mode 100644 packages/web/src/layouts/AdrBrowserLayout/components/RoutingProgress/index.ts create mode 100644 packages/web/src/layouts/AdrBrowserLayout/components/index.ts create mode 100644 packages/web/src/layouts/AdrBrowserLayout/index.ts create mode 100644 packages/web/src/layouts/index.ts create mode 100644 packages/web/src/lib/adr-utils.ts create mode 100644 packages/web/src/lib/console.ts create mode 100644 packages/web/src/lib/core-api/getIndexPageMarkdown.ts create mode 100644 packages/web/src/lib/core-api/index.ts create mode 100644 packages/web/src/lib/core-api/instance.ts create mode 100644 packages/web/src/lib/core-api/noop/.log4brains.yml create mode 100644 packages/web/src/lib/core-api/noop/noop-adrs/.gitignore create mode 100644 packages/web/src/lib/debug.ts create mode 100644 packages/web/src/lib/next.ts create mode 100644 packages/web/src/lib/search/Search.ts create mode 100644 packages/web/src/lib/search/index.ts create mode 100644 packages/web/src/lib/search/instance.ts create mode 100644 packages/web/src/lib/slugify.ts create mode 100644 packages/web/src/lib/toc-utils/Toc.ts create mode 100644 packages/web/src/lib/toc-utils/TocBuilder.test.ts create mode 100644 packages/web/src/lib/toc-utils/TocBuilder.ts create mode 100644 packages/web/src/lib/toc-utils/TocContainer.ts create mode 100644 packages/web/src/lib/toc-utils/TocSection.ts create mode 100644 packages/web/src/lib/toc-utils/index.ts create mode 100644 packages/web/src/mui/MuiDecorator.tsx create mode 100644 packages/web/src/mui/index.ts create mode 100644 packages/web/src/mui/theme.ts create mode 100644 packages/web/src/pages/_app.tsx create mode 100644 packages/web/src/pages/_document.tsx create mode 100644 packages/web/src/pages/adr/[...slug].tsx create mode 100644 packages/web/src/pages/api/adr.ts create mode 100644 packages/web/src/pages/api/adr/[...slugAndMore].ts create mode 100644 packages/web/src/pages/api/search-index.ts create mode 100644 packages/web/src/pages/index.tsx create mode 100644 packages/web/src/scenes/AdrScene/AdrScene.stories.tsx create mode 100644 packages/web/src/scenes/AdrScene/AdrScene.tsx create mode 100644 packages/web/src/scenes/AdrScene/components/AdrHeader/AdrHeader.test.tsx create mode 100644 packages/web/src/scenes/AdrScene/components/AdrHeader/AdrHeader.tsx create mode 100644 packages/web/src/scenes/AdrScene/components/AdrHeader/__snapshots__/AdrHeader.test.tsx.snap create mode 100644 packages/web/src/scenes/AdrScene/components/AdrHeader/index.ts create mode 100644 packages/web/src/scenes/AdrScene/components/index.ts create mode 100644 packages/web/src/scenes/AdrScene/index.ts create mode 100644 packages/web/src/scenes/IndexScene/IndexScene.stories.tsx create mode 100644 packages/web/src/scenes/IndexScene/IndexScene.tsx create mode 100644 packages/web/src/scenes/IndexScene/index.ts create mode 100644 packages/web/src/scenes/index.ts create mode 100644 packages/web/src/types.ts create mode 100644 packages/web/tsconfig.dev.json create mode 100644 packages/web/tsconfig.json create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..91e96322 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..8e4b29b9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,85 @@ +module.exports = { + root: true, + env: { + es2021: true + }, + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 12, + sourceType: "module", + project: "./tsconfig.json" + }, + ignorePatterns: ["**/*.js", "**/*.d.ts", "dist", "node_modules"], + plugins: ["jest", "sonarjs", "promise", "@typescript-eslint", "react"], + extends: [ + "eslint:recommended", + "plugin:jest/recommended", + "plugin:jest/style", + "plugin:sonarjs/recommended", + "plugin:promise/recommended", + "airbnb-typescript", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier", + "prettier/@typescript-eslint", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "airbnb/hooks", + "prettier/react" + ], + rules: { + "import/prefer-default-export": "off", // @adr 20200927-avoid-default-exports + "import/no-default-export": "error", // @adr 20200927-avoid-default-exports + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }], + "sonarjs/no-duplicate-string": "off", + "react/prefer-stateless-function": [2, { ignorePureComponents: false }], + "react/function-component-definition": [ + 2, + { + namedComponents: "function-declaration", + unnamedComponents: "arrow-function" + } + ], + "react/require-default-props": [2, { ignoreFunctionalComponents: true }], // DefaultProps are deprecated for functional components (https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/require-default-props.md) + "react/jsx-props-no-spreading": "off", // For HOC + "no-void": [2, { allowAsStatement: true }], // For React.useEffect() with async functions + "react/display-name": "off" + }, + overrides: [ + { + files: ["*.ts", "*.tsx"] // Forces Jest to include these files + }, + { + files: "*.tsx", + rules: { + "sonarjs/cognitive-complexity": [2, 18], // React functions are usually more complex + "@typescript-eslint/explicit-module-boundary-types": "off" // @adr web/20200927-avoid-react-fc-type + } + }, + { + files: ["src/**/*.stories.tsx"], // Storybook + rules: { + "import/no-extraneous-dependencies": "off", + "import/no-default-export": "off", + "react/function-component-definition": "off" + } + }, + { + files: "*", // All non-React files + excludedFiles: "*.tsx", + rules: { + "react/static-property-placement": "off" + } + }, + { + files: "**.test.ts", // Jest + rules: { + "max-classes-per-file": "off", + "import/no-extraneous-dependencies": "off", + "no-new": "off", + "no-empty": "off" + } + } + ] +}; diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000..790260df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,43 @@ +--- +name: "Bug Report" +about: "Report a bug" +labels: bug +--- + +# Bug Report + +## Description + + + +## Steps to Reproduce + + + + + +1. Step 1 +2. Step 2 +3. ... + +## Expected Behavior + + + +## Context + + + + +## Environment + + + +- Log4brains version: +- Node.js version: +- OS and its version: +- Browser information: + +## Possible Solution + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..4360118e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: Give your feedback 📣 + url: https://github.com/thomvaill/log4brains/discussions/new?category=Feedback + about: Give your feedback (positive or negative!) on the BETA version + - name: Ask a question + url: https://github.com/thomvaill/log4brains/discussions + about: Ask questions and discuss with other community members diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..b5fc75cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,22 @@ +--- +name: "Feature Request" +about: "Suggest new features and changes" +labels: feature +--- + +# Feature Request + +## Feature Suggestion + + + +## Context + + + + +## Possible Implementation + + + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..64949a9c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,230 @@ +# Inspired from https://github.com/backstage/backstage/blob/master/.github/workflows/ci.yml. Thanks! +# See ci.yml for info on `quality` and `tests` stages +name: Build +on: + push: + branches: + - master + +jobs: + quality: + strategy: + # We use a matrix even if it's not needed here to be able to re-use the same Yarn setup snippet everywhere + matrix: + os: [ubuntu-latest] + node-version: [14.x] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + + # Beginning of yarn setup [KEEP IN SYNC BETWEEN ALL WORKFLOWS] (copy/paste of ci.yml's snippet) + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: "**/node_modules" + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + if: steps.cache-modules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + # End of yarn setup + + # TODO: make dev & test work without having to build core & cli-common (inspiration: https://github.com/Izhaki/mono.ts) + - name: build core + run: yarn build --scope @log4brains/core --scope @log4brains/cli-common + + - name: format + run: yarn format + + - name: lint + run: yarn lint + + tests: + needs: quality + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [14.x, 12.x, 10.x] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # fetch all history to make Jest snapshot tests work + + # Beginning of yarn setup [KEEP IN SYNC BETWEEN ALL WORKFLOWS] (copy/paste of ci.yml's snippet) + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: "**/node_modules" + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + if: steps.cache-modules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + # End of yarn setup + + # We have to build all the packages before the tests + # Because init-log4brains's integration tests use @log4brains/cli, which uses @log4brains/core + # TODO: we should separate tests that require built packages of the others, to get a quicker feedback + # Once it's done, we should add "yarn test" in each package's preVersion script + - name: build + run: yarn build && yarn links + + - name: test + run: yarn test + + - name: E2E tests + run: yarn e2e + + publish-pages: + needs: tests + strategy: + # We use a matrix even if it's not needed here to be able to re-use the same Yarn setup snippet everywhere + matrix: + os: [ubuntu-latest] + node-version: [14.x] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + persist-credentials: false # required by JamesIves/github-pages-deploy-action + fetch-depth: 0 # required by Log4brains to work correctly (needs the whole Git history) + + # Beginning of yarn setup [KEEP IN SYNC BETWEEN ALL WORKFLOWS] (copy/paste of ci.yml's snippet) + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: "**/node_modules" + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + if: steps.cache-modules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + # End of yarn setup + + - name: build + run: yarn build + + - name: build self knowledge base + env: + HIDE_LOG4BRAINS_VERSION: "1" # TODO: use lerna to bump the version temporarily here so we don't have to hide it + run: yarn log4brains-build --basePath /${GITHUB_REPOSITORY#*/}/adr + + - name: publish self knowledge base + uses: JamesIves/github-pages-deploy-action@3.7.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: .log4brains/out + TARGET_FOLDER: adr + + release: + needs: publish-pages # we could perform this step in parallel but this acts as a last end-to-end test before releasing + strategy: + # We use a matrix even if it's not needed here to be able to re-use the same Yarn setup snippet everywhere + matrix: + os: [ubuntu-latest] + node-version: [14.x] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # fetch all history for Lerna (https://stackoverflow.com/a/60184319/9285308) + + - name: fetch all git tags for Lerna # (https://stackoverflow.com/a/60184319/9285308) + run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + # Beginning of yarn setup [KEEP IN SYNC BETWEEN ALL WORKFLOWS] (copy/paste of ci.yml's snippet) + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: "**/node_modules" + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + if: steps.cache-modules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + # End of yarn setup + + - name: build + run: yarn build + + - name: git identity + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: release and publish to NPM + # TODO: change when going stable + run: yarn lerna publish --yes --conventional-commits --conventional-prerelease --force-publish --create-release github + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..92ae6746 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,133 @@ +# Inspired from https://github.com/backstage/backstage/blob/master/.github/workflows/ci.yml. Thanks! +name: CI +on: + pull_request: + +jobs: + quality: + strategy: + # We use a matrix even if it's not needed here to be able to re-use the same Yarn setup snippet everywhere + matrix: + os: [ubuntu-latest] + node-version: [14.x] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + + # Beginning of yarn setup [KEEP IN SYNC BETWEEN ALL WORKFLOWS] + # TODO: create a dedicated composite GitHub Action to avoid copy/pastes everywhere + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # needed for auth when publishing + + # Cache every node_modules folder inside the monorepo + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: "**/node_modules" + # We use both yarn.lock and package.json as cache keys to ensure that + # changes to local monorepo packages bust the cache. + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + + # If we get a cache hit for node_modules, there's no need to bring in the global + # yarn cache or run yarn install, as all dependencies will be installed already. + + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + if: steps.cache-modules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + # End of yarn setup + + # TODO: make dev & test work without having to build core & cli-common (inspiration: https://github.com/Izhaki/mono.ts) + - name: build core + run: yarn build --scope @log4brains/core --scope @log4brains/cli-common + + - name: format + run: yarn format + + - name: lint + run: yarn lint + + tests: + needs: quality + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [14.x, 12.x, 10.x] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # fetch all history to make Jest snapshot tests work + + - name: fetch branch master + run: git fetch origin master + + # Beginning of yarn setup [KEEP IN SYNC BETWEEN ALL WORKFLOWS] (copy/paste of the snippet above) + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: "**/node_modules" + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + if: steps.cache-modules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + # End of yarn setup + + - name: check for yarn.lock changes + id: yarn-lock + run: git diff --quiet origin/master HEAD -- yarn.lock + continue-on-error: true + # - steps.yarn-lock.outcome == 'success' --> yarn.lock was not changed + # - steps.yarn-lock.outcome == 'failure' --> yarn.lock was changed + + # We have to build all the packages before the tests + # Because init-log4brains's integration tests use @log4brains/cli, which uses @log4brains/core + # TODO: we should separate tests that require built packages of the others, to get a quicker feedback + # Once it's done, we should add "yarn test" in each package's preVersion script + - name: build + run: yarn build && yarn links + + - name: test changed packages + if: ${{ steps.yarn-lock.outcome == 'success' }} + run: yarn test --since origin/master + + - name: test all packages + if: ${{ steps.yarn-lock.outcome == 'failure' }} + run: yarn test + + - name: E2E tests + run: yarn e2e diff --git a/.gitignore b/.gitignore index 67045665..7068d9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ dist # TernJS port file .tern-port + +# Log4brains +.log4brains diff --git a/.log4brains.yml b/.log4brains.yml new file mode 100644 index 00000000..88d31ffa --- /dev/null +++ b/.log4brains.yml @@ -0,0 +1,14 @@ +--- +project: + name: Log4brains + tz: Europe/Paris + adrFolder: ./docs/adr + + packages: + - name: core + path: ./packages/core + adrFolder: ./packages/core/docs/adr + + - name: web + path: ./packages/web + adrFolder: ./packages/web/docs/adr diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..01487040 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules/ +out/ +dist/ +CHANGELOG.md +/lerna.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..29e67bf6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,46 @@ +{ + "editor.formatOnSave": true, + "[typescript]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[javascript]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "cSpell.words": [ + "Diagnosticable", + "Transpiled", + "adrs", + "awilix", + "clsx", + "copyfiles", + "diagnotics", + "esnext", + "execa", + "gitlab", + "globby", + "htmlentities", + "lunr", + "microbundle", + "neverthrow", + "outdir", + "signale", + "typedoc", + "unversioned", + "workdir" + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..990fd31e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +thomvaill@bluebricks.dev. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..712a4797 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing to Log4brains + +:+1::tada: First of all, thanks for taking the time to contribute! :tada::+1: + +All your contributions are very welcome, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing new features +- Becoming a maintainer + +Thank you so much! :clap: + +## Development + +```bash +yarn install +yarn links +yarn dev + +# You can now develop +# `yarn dev` re-builds the changed packages live + +# You can test the different packages directly on the Log4brains project +# All its scripts are linked to your local dev version +yarn adr new +yarn log4brains-build +yarn serve # serves the build output (`.log4brains/out`) locally + +# For the Next.js app, you can enable the Fast Refresh feature just by setting NODE_ENV to `development` +NODE_ENV=development yarn log4brains-preview +# Or you can run this more convenient command on the root project: +yarn log4brains-preview:dev +# Or if you want to debug only the Next.js app without the Log4brains custom part, you can run: +cd packages/web && yarn next dev # (in this case `yarn dev` is not needed before running this command) + +# To work on the UI, you probably would like to use the Storybook instead: +cd packages/web && yarn storybook + +# You can also test the different packages on an empty project +# `npx init-log4brains` is linked to your local dev version and will install +# the dev version of each Log4brains package if you set NODE_ENV to `development` +cd $(mktemp -d -t l4b-test-XXXX) && npm init --yes && npm install +NODE_ENV=development npx init-log4brains +``` + +## Checks to run before pushing + +```bash +yarn lint # enforced automatically before every commit with husky+lint-staged +yarn format:fix # enforced automatically before every commit with husky+lint-staged +yarn test:changed # (or `yarn test` to run all the tests) +``` + +Please do not forget to add tests to your contribution if this is applicable! + +## License + +By contributing to Log4brains, you agree that your contributions will be licensed under its Apache 2.0 License. diff --git a/LICENSE b/LICENSE index 261eeb9e..ea34bfdc 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 Thomas Vaillant Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 17467331..e3b19769 100644 --- a/README.md +++ b/README.md @@ -1 +1,514 @@ -# log4brains \ No newline at end of file +# Log4brains + +

+ + Log4brains logo + +

+ +

+ + License + + + Build Status + + + @log4brains/web latest version + + + @log4brains/cli latest version + +

+ +Log4brains is a docs-as-code knowledge base for your development and infrastructure projects. +It enables you to write and manage [Architecture Decision Records](https://adr.github.io/) (ADR) right from your IDE, and to publish them automatically as a static website. + +
+Features +

+ +- Docs-as-code: ADRs are written in markdown, stored in your git repository, close to your code +- Local preview with Hot Reload +- Interactive ADR creation from the CLI +- Static site generation to publish to GitHub/GitLab Pages or S3 +- Timeline menu +- Searchable +- ADR metadata automatically guessed from its raw text and git logs +- No enforced markdown structure: you are free to write however you want +- No required file numbering schema (i.e., `adr-0001.md`, `adr-0002.md`...): avoids git merge issues +- Customizable template (default: [MADR](https://adr.github.io/madr/)) +- Multi-package projects support (mono or multi repo): notion of global and package-specific ADRs + +**Coming soon**: + +- Local images and diagrams support +- RSS feed to be notified of new ADRs +- Decision backlog +- `@adr` annotation to include code references in ADRs +- ADR creation/edition from the UI +- Create a new GitHub/GitLab issue from the UI +- ... let's [suggest a new feature](https://github.com/thomvaill/log4brains/issues/new?labels=feature&template=feature.md) if you have other needs! + +

+
+ +
+

+ + Log4brains demo + +

+

Demo: Log4brains' own architecture knowledge base

+ +## Table of contents + +- [📣 Beta version: your feedback is welcome!](#-beta-version-your-feedback-is-welcome) +- [🚀 Getting started](#-getting-started) +- [🤔 What is an ADR and why should you use them](#-what-is-an-adr-and-why-should-you-use-them) +- [💡 Why Log4brains](#-why-log4brains) +- [📨 CI/CD configuration examples](#-cicd-configuration-examples) +- [❓ FAQ](#-faq) + - [What are the prerequisites?](#what-are-the-prerequisites) + - [What about multi-package projects?](#what-about-multi-package-projects) + - [What about non-JS projects?](#what-about-non-js-projects) + - [How to configure `.log4brains.yml`?](#how-to-configure-log4brainsyml) +- [Contributing](#contributing) +- [Acknowledgments](#acknowledgments) +- [License](#license) + +## 📣 Beta version: your feedback is welcome! + +At this stage, Log4brains is just a few months old and was designed only based on my needs and my past experiences with ADRs. +But I am convinced that this project can benefit a lot of teams. +This is why it would be precious for me to get your feedback on this beta version in order to improve it. + +To do so, you are very welcome to [create a new feedback in the Discussions](https://github.com/thomvaill/log4brains/discussions/new?category=Feedback) or to reach me at . Thanks a lot 🙏 + +## 🚀 Getting started + +According to the Log4brains philosophy, you should store your Architecture Decision Records (ADR) the closest to your code, which means ideally inside your project's git repository, for example in `/docs/adr`. In the case of a JS project, we recommend installing Log4brains as a dev dependency. To do so, run our interactive setup CLI inside your project root directory: + +```bash +npx init-log4brains +``` + +... it will ask you several questions to get your knowledge base installed and configured properly. Click [here](#what-about-non-js-projects) for non-JS projects. + +Then, you can start the web UI in local preview mode: + +```bash +npm run log4brains-preview + +# OR + +yarn log4brains-preview +``` + +In this mode, the Hot Reload feature is enabled: any change +you make to a markdown file is applied live in the UI. + +You can use this command to easily create a new ADR interactively: + +```bash +npm run adr -- new + +# OR + +yarn adr new +``` + +Just add the `--help` option for more information on this command. + +Finally, do not forget to [set up your CI/CD pipeline](#-cicd-configuration-examples) to automatically publish your knowledge base on a static website service like GitHub/GitLab Pages or S3. + +## 🤔 What is an ADR and why should you use them + +The term ADR become popular in 2011 with Michael Nygard's article: [documenting architecture decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions). He aimed to reconcile Agile methods with software documentation by creating a very concise template +to record functional or non-functional "architecturally significant" decisions in a lightweight format like markdown. +The original template had only a few parts: + +- **Title**: Which sums up the solved problem and its solution +- **Context**: Probably the essential part, which describes "the forces at play, including technological, political, social, and project local" +- **Decision** +- **Status**: Proposed, accepted, deprecated, superseded... +- **Consequences**: The positive and negative ones for the future of the project + +Today, there are other ADR templates like [Y-Statements](https://medium.com/olzzio/y-statements-10eb07b5a177), or [MADR](https://adr.github.io/madr/), which is the default one that is configured in Log4brains. +Anyway, we believe that no template suits everyone's needs. You should adapt it according to your own situation. + +As you can guess from the template above, an ADR is immutable. Only its status can change. +Thanks to this, your documentation is never out-of-date! Yes, an ADR can be deprecated or superseded by another one, but it was at least true one day! +And even if it's not the case anymore, it is still a precious piece of information. + +This leads us to the main goals of this methodology: + +- Avoid blind acceptance and blind reversal when you face past decisions +- Speed up the onboarding of new developers on a project +- Formalize a collaborative decision-making process + +The first goal was the very original one, intended by Michael Nygard in his article. +I discovered the two others in my past experiences with ADRs, and this is why I decided to create Log4brains. + +To learn more on this topic, I recommend you to read these great resources: + +- [Documenting architecture decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions), by Michael Nygard +- [ADR GitHub organization](https://adr.github.io/), home of the [MADR](https://adr.github.io/madr/) template, by @boceckts and @koppor +- [Collection of ADR templates](https://github.com/joelparkerhenderson/architecture_decision_record) by @joelparkerhenderson + +## 💡 Why Log4brains + +I've been using ADRs for a long time and, I often introduce this methodology to the teams I work with as a freelance developer. +It's always the same scenario: first, no one had ever heard about ADRs, and after using them for a while, they realize [how useful yet straightforward they are](#-what-is-an-adr-and-why-should-you-use-them). So one of the reasons I decided to start working on Log4brains was to popularize this methodology. + +On the other hand, I wanted to solve some issues I encountered with them, like improving their discoverability or the poor tooling around them. +But above all, I am convinced that ADRs can have a broader impact than what they were intended for: speed up the onboarding on a project by becoming a training material, and become the support of a collaborative decision-making process. + +In the long term, I see Log4brains as part of a global strategy that would let companies build and capitalize their teams' technical knowledge collaboratively. + +## 📨 CI/CD configuration examples + +Log4brains lets you publish automatically your knowledge base on the static hosting service of your choice, thanks to the `log4brains-web build` command. +Here are some configuration examples for the most common hosting services / CI runners. + +
+Publish to GitHub Pages with GitHub Actions +

+ +First, create `.github/workflows/publish-log4brains.yml` and adapt it to your case: + +```yml +name: Publish Log4brains +on: + push: + branches: + - master +jobs: + build-and-publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + with: + persist-credentials: false # required by JamesIves/github-pages-deploy-action + fetch-depth: 0 # required by Log4brains to work correctly (needs the whole Git history) + - name: Install Node + uses: actions/setup-node@v1 + with: + node-version: "14" + # NPM: + # (unfortunately, we cannot use `npm ci` for now because of this bug: https://github.com/npm/cli/issues/558) + - name: Install and Build Log4brains (NPM) + run: | + npm install + npm run log4brains-build -- --basePath /${GITHUB_REPOSITORY#*/}/log4brains + # Yarn: + # - name: Install and Build Log4brains (Yarn) + # run: | + # yarn install --frozen-lockfile + # yarn log4brains-build --basePath /${GITHUB_REPOSITORY#*/}/log4brains + - name: Deploy + uses: JamesIves/github-pages-deploy-action@3.7.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: .log4brains/out + TARGET_FOLDER: log4brains +``` + +After the first run, this workflow will create a `gh-pages` branch in your repository containing the generated static files to serve. +Then, we have to tell GitHub that we [don't want to use Jekyll](https://github.com/vercel/next.js/issues/2029), otherwise, you will get a 404 error: + +```bash +git checkout gh-pages +touch .nojekyll +git add .nojekyll +git commit -m "Add .nojekyll for Log4brains" +git push +``` + +Finally, you can [enable your GitHub page](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site): + +- On GitHub, go to `Settings > GitHub Pages` +- Select the `gh-pages` branch as the "Source" +- Then, select the `/ (root)` folder + +You should now be able to see your knowledge base at `https://.github.io//log4brains/`. +It will be re-built and published every time you push on `master`. + +

+
+ +
+Publish to GitLab Pages with GitLab CI +

+ +Create your `.gitlab-ci.yml` and adapt it to your case: + +```yml +image: node:14-alpine3.12 +pages: + stage: deploy + variables: + GIT_DEPTH: 0 # required by Log4brains to work correctly (needs the whole Git history) + script: + - mkdir -p public + # NPM: + - npm install # unfortunately we cannot use `npm ci` for now because of this bug: https://github.com/npm/cli/issues/558 + - npm run log4brains-build -- --basePath /$CI_PROJECT_NAME/log4brains --out public/log4brains + # Yarn: + # - yarn install --frozen-lockfile + # - yarn log4brains-build --basePath /$CI_PROJECT_NAME/log4brains --out public/log4brains + artifacts: + paths: + - public + rules: + - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH" +``` + +You should now be able to see your knowledge base at `https://.gitlab.io//log4brains/`. +It will be re-built and published every time you push on `master`. + +

+
+ +
+Publish to S3 +

+ +First, create a bucket with the "Static website hosting" feature enabled: + +```bash +# This is an example: replace with the bucket name of your choice +export BUCKET_NAME=yourcompany-yourproject-log4brains + +aws s3api create-bucket --acl public-read --bucket ${BUCKET_NAME} +read -r -d '' BUCKET_POLICY << EOP +{ + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::${BUCKET_NAME}/*" + } + ] +} +EOP +aws s3api put-bucket-policy --bucket ${BUCKET_NAME} --policy "$BUCKET_POLICY" +aws s3 website s3://${BUCKET_NAME} --index-document index.html +``` + +Then, configure your CI to run these commands: + +- Install Node and the AWS CLI +- Checkout your Git repository **with the full history**. Otherwise, Log4brains won't work correctly (see previous examples) +- `npm install` or `yarn install --frozen-lockfile` to install the dev dependencies (unfortunately we cannot use `npm ci` for now because of this [bug](https://github.com/npm/cli/issues/558)) +- `npm run log4brains-build` or `yarn log4brains-build` +- `aws s3 sync .log4brains/out s3:// --delete` + +Your knowledge base will be available on `http://.s3-website-.amazonaws.com/`. +You can get some inspiration on implementing this workflow for GitHub Actions or GitLab CI by looking at the previous examples. + +

+
+ +## ❓ FAQ + +### What are the prerequisites? + +- Node.js >= 10.23 +- NPM or Yarn +- Your project versioned in Git ([not necessarily a JS project!](#what-about-non-js-projects)) + +### What about multi-package projects? + +Log4brains supports both mono and multi packages projects. The `npx init-log4brains` command will prompt you regarding this. + +In the case of a multi-package project, you have two options: + +- Mono-repository: in this case, just install Log4brains in the root folder. It will manage "global ADRs", for example in `docs/adr` and "package-specific ADRs", for example in `packages//docs/adr`. +- One repository per package: in the future, Log4brains will handle this case with a central repository for the "global ADRs" while fetching "package-specifics ADRs" directly from each package repository. For the moment, all the ADRs have to be stored in a central repository. + +Here is an example of a typical file structure for each case: + +
+Simple mono-package project +

+ +``` +project-root +├── docs +| └── adr +| ├── 20200101-your-first-adr.md +| ├── 20200115-your-second-adr.md +| ├── [...] +| ├── index.md +| └── template.md +[...] +``` + +

+
+ +
+Multi-package project in a mono-repository +

+ +``` +project-root +├── docs +| └── adr +| ├── 20200101-your-first-global-adr.md +| ├── 20200115-your-second-global-adr.md +| ├── [...] +| ├── index.md +| └── template.md +├── packages +| ├── package1 +| | ├── docs +| | | └── adr +| | | ├── 20200102-your-first-package-specific-adr.md +| | | ├── 20200116-your-second-package-specific-adr.md +| | | [...] +| | [...] +| ├── package2 +| | ├── docs +| | | └── adr +| | | ├── [...] +| | | [...] +| | [...] +| [...] +[...] +``` + +

+
+ +
+Multi-package with one repository per package +

+ +For the moment in one central repository (specific for the docs, or not): + +``` +project-docs +├── adr +| ├── global +| | ├── 20200101-your-first-global-adr.md +| | ├── 20200115-your-second-global-adr.md +| | ├── [...] +| | ├── index.md +| | └── template.md +| ├── package1 +| | ├── 20200102-your-first-package-specific-adr.md +| | ├── 20200116-your-second-package-specific-adr.md +| | [...] +| ├── package2 +| | ├── [...] +| | [...] +| [...] +[...] +``` + +In the future: + +``` +project-docs +├── adr +| ├── 20200101-your-first-global-adr.md +| ├── 20200115-your-second-global-adr.md +| ├── [...] +| ├── index.md +| └── template.md +[...] + +repo1 +├── docs +| └── adr +| ├── 20200102-your-first-package-specific-adr.md +| ├── 20200116-your-second-package-specific-adr.md +| [...] +[...] + +repo2 +├── docs +| └── adr +| ├── [...] +| [...] +[...] +``` + +

+
+ +### What about non-JS projects? + +Even if Log4brains is developed with TypeScript and is part of the NPM ecosystem, it can be used for any kind of project, in any language. + +For projects that do not have a `package.json` file, you have to install Log4brains globally: + +```bash +npm install -g @log4brains-cli @log4brains-web +``` + +Create a `.log4brains.yml` file at the root of your project and [configure it](#how-to-configure-log4brainsyml). + +You can now use these global commands inside your project: + +- Create a new ADR: `log4brains adr new` +- Start the local web UI: `log4brains-web preview` +- Build the static version: `log4brains-web build` + +### How to configure `.log4brains.yml`? + +This file is usually automatically created when you run `npx init-log4brains` (cf [getting started](#-getting-started)), but you may need to configure it manually. + +Here is an example with just the required fields: + +```yaml +project: + name: Foo Bar # The name that should be displayed in the UI + tz: Europe/Paris # The timezone that you use for the dates in your ADR files + adrFolder: ./docs/adr # The location of your ADR files +``` + +If you have multiple packages in your project, you may want to support package-specific ADRs by setting the optional `project.packages` field: + +```yaml +project: + # [...] + packages: + - name: backend # The name (unique identifier) of the package + path: ./packages/backend # The location of its codebase + adrFolder: ./packages/backend/docs/adr # The location of its ADR files +# - ... +``` + +Another optional field is `project.repository`, which is normally automatically guessed by Log4brains to create links to GitHub, GitLab, etc. But in some cases, like for GitHub or GitLab enterprise, you have to configure it manually: + +```yaml +project: + # [...] + repository: + url: https://github.com/foo/bar # Absolute URL of your repository + provider: github # Supported providers: github, gitlab, bitbucket. Use `generic` if yours is not supported + viewFileUriPattern: /blob/%branch/%path # Only required for `generic` providers +``` + +## Contributing + +Pull Requests are more than welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for more details. You can also [create a new issue](https://github.com/thomvaill/log4brains/issues/new/choose) or [give your feedback](https://github.com/thomvaill/log4brains/discussions/new?category=Feedback). + +## Acknowledgments + +- [Next.js](https://github.com/vercel/next.js/), which is used under the hood to provide the web UI and the static site generation capability (look for `#NEXTJS-HACK` in the code to see the custom adaptations we had to make) +- Michael Nygard for all his work on [Architecture Decision Records](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) +- @boceckts and @koppor for the [MADR](https://adr.github.io/madr/) template +- [Tippawan Sookruay](https://thenounproject.com/wanny4/) for the Log4brains logo +- @npryce, who inspired me for the CLI part with his [adr-tools](https://github.com/npryce/adr-tools) bash CLI +- @mrwilson, who inspired me for the static site generation part with his [adr-viewer](https://github.com/mrwilson/adr-viewer) + +## License + +This project is licensed under the Apache 2.0 license, Copyright (c) 2020 Thomas Vaillant. See the [LICENSE](LICENSE) file for more information. diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..5073c20d --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +module.exports = { extends: ["@commitlint/config-conventional"] }; diff --git a/docs/Log4brains-logo-full.png b/docs/Log4brains-logo-full.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa2d7d6ad9994707f7a631b013abb489a2da40b GIT binary patch literal 31547 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 literal 0 HcmV?d00001 diff --git a/docs/adr/20200924-use-markdown-architectural-decision-records.md b/docs/adr/20200924-use-markdown-architectural-decision-records.md new file mode 100644 index 00000000..e4ad2f5b --- /dev/null +++ b/docs/adr/20200924-use-markdown-architectural-decision-records.md @@ -0,0 +1,37 @@ +# Use Markdown Architectural Decision Records + +- Status: accepted +- Date: 2020-09-24 + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which format and structure should these records follow? + +## Considered Options + +- [MADR](https://adr.github.io/madr/) 2.1.2 with Log4brains patch +- [MADR](https://adr.github.io/madr/) 2.1.2 – The original Markdown Architectural Decision Records +- [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +- [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements +- Other templates listed at +- Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 2.1.2 with Log4brains patch", because + +- Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +- The MADR format is lean and fits our development style. +- The MADR structure is comprehensible and facilitates usage & maintenance. +- The MADR project is vivid. +- Version 2.1.2 is the latest one available when starting to document ADRs. +- The Log4brains patch adds more features, like tags. + +The "Log4brains patch" performs the following modifications to the original template: + +- Change the ADR filenames format (`NNN-adr-name` becomes `YYYYMMDD-adr-name`), to avoid conflicts during Git merges. +- Add a `draft` status, to enable collaborative writing. +- Add a `Tags` field. diff --git a/docs/adr/20200925-multi-packages-architecture-in-a-monorepo-with-yarn-and-lerna.md b/docs/adr/20200925-multi-packages-architecture-in-a-monorepo-with-yarn-and-lerna.md new file mode 100644 index 00000000..d83adb04 --- /dev/null +++ b/docs/adr/20200925-multi-packages-architecture-in-a-monorepo-with-yarn-and-lerna.md @@ -0,0 +1,40 @@ +# Multi-packages architecture in a monorepo with Yarn and Lerna + +- Status: accepted +- Date: 2020-09-25 + +## Context and Problem Statement + +We have to define the initial overall architecture of the project. +For now, we are sure that we want to provide these features: + +- Local preview web UI +- Static Site Generation from the CI/CD +- CLI to create a new ADR quickly + +In the future, we might want to provide these features: + +- Create/edit ADRs from the local web UI +- VSCode extension to create and maybe edit an ADR from the IDE +- Support ADR aggregation from multiple repositories + +## Considered Options + +- Monolith +- Multi-packages, multirepo +- Multi-packages, monorepo + - with NPM and scripts for links and publication + - with Yarn and scripts for publication + - with Yarn and Lerna + +## Decision Outcome + +Chosen option: "Multi-packages, monorepo, with Yarn and Lerna", because + +- We don't want a monolith because we want the core library/API to be very well tested and probably developed with DDD and hexagonal architecture. The other packages will just call this core API, they will contain fewer business rules as possible. As we are not so sure about the features we will provide in the future, this is good for extensibility. +- Yarn + Lerna seems to be a very good practice used by a lot of other open-source projects to publish npm packages. + +## Links + +- [A Beginner's Guide to Lerna with Yarn Workspaces](https://medium.com/@jsilvax/a-workflow-guide-for-lerna-with-yarn-workspaces-60f97481149d) +- [Step by Step Guide to create a Typescript Monorepo with Yarn Workspaces and Lerna](https://blog.usejournal.com/step-by-step-guide-to-create-a-typescript-monorepo-with-yarn-workspaces-and-lerna-a8ed530ecd6d) diff --git a/docs/adr/20200925-use-prettier-eslint-airbnb-for-the-code-style.md b/docs/adr/20200925-use-prettier-eslint-airbnb-for-the-code-style.md new file mode 100644 index 00000000..ad87368b --- /dev/null +++ b/docs/adr/20200925-use-prettier-eslint-airbnb-for-the-code-style.md @@ -0,0 +1,33 @@ +# Use Prettier-ESLint Airbnb for the code style + +- Status: accepted +- Date: 2020-09-25 + +## Context and Problem Statement + +We have to choose our lint and format tools, and the code style to enforce as well. + +## Considered Options + +- Prettier only +- ESLint only +- ESLint with Airbnb code style +- ESLint with StandardJS code style +- ESLint with Google code style +- Prettier-ESLint with Airbnb code style +- Prettier-ESLint with StandardJS code style +- Prettier-ESLint with Google code style + +## Decision Outcome + +Chosen option: "Prettier-ESLint with Airbnb code style", because + +- Airbnb code style is widely used (see [npm trends](https://www.npmtrends.com/eslint-config-airbnb-vs-eslint-config-google-vs-standard-vs-eslint-config-standard)) +- Prettier-ESLint enforce some additional code style. We like it because the more opinionated the code style is, the less debates there will be :-) + +In addition, we use also Prettier to format json and markdown files. + +### Positive Consequences + +- Developers are encouraged to use the [Prettier ESLint](https://marketplace.visualstudio.com/items?itemName=rvest.vs-code-prettier-eslint) and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) VSCode extensions while developing to auto-format the files on save +- And they are encouraged to use the [ESLint VS Code extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) as well to highlight linting issues while developing diff --git a/docs/adr/20200926-use-the-adr-number-as-its-unique-id.md b/docs/adr/20200926-use-the-adr-number-as-its-unique-id.md new file mode 100644 index 00000000..3728f4d6 --- /dev/null +++ b/docs/adr/20200926-use-the-adr-number-as-its-unique-id.md @@ -0,0 +1,25 @@ +# Use the ADR number as its unique ID + +- Status: superseded by [20201016-use-the-adr-slug-as-its-unique-id](20201016-use-the-adr-slug-as-its-unique-id.md) +- Date: 2020-09-26 + +## Context and Problem Statement + +We need to be able to identify uniquely an ADR, especially in these contexts: + +- Web: to build its URL +- CLI: to identify an ADR in a command argument (example: "edit", or "preview") + +## Considered Options + +- ADR number (ie. filename prefixed number, example: `0001-use-markdown-architectural-decision-records.md`) +- ADR filename +- ADR title + +## Decision Outcome + +Chosen option: "ADR number", because + +- It is possible to have duplicated titles +- The filename is too long to enter without autocompletion, but we could support it as a second possible identifier for the CLI in the future +- Other ADR tools like [adr-tools](https://github.com/npryce/adr-tools) already use the number as a unique ID diff --git a/docs/adr/20200927-avoid-default-exports.md b/docs/adr/20200927-avoid-default-exports.md new file mode 100644 index 00000000..4d53b9bd --- /dev/null +++ b/docs/adr/20200927-avoid-default-exports.md @@ -0,0 +1,13 @@ +# Avoid default exports + +- Status: accepted +- Date: 2020-09-27 + +## Decision + +We will avoid default exports in all our codebase, and use named exports instead. + +## Links + +- +- diff --git a/docs/adr/20201016-use-the-adr-slug-as-its-unique-id.md b/docs/adr/20201016-use-the-adr-slug-as-its-unique-id.md new file mode 100644 index 00000000..2cd9ced1 --- /dev/null +++ b/docs/adr/20201016-use-the-adr-slug-as-its-unique-id.md @@ -0,0 +1,30 @@ +# Use the ADR slug as its unique ID + +- Status: accepted +- Date: 2020-10-16 + +## Context and Problem Statement + +Currently, ADR files follow this format: `NNNN-adr-title.md`, with NNNN being an incremental number from `0000` to `9999`. +It causes an issue during a `git merge` when two developers have created a new ADR on their respective branch. +There is a conflict because [an ADR number must be unique](20200926-use-the-adr-number-as-its-unique-id.md). + +## Decision + +From now on, we won't use ADR numbers anymore. +An ADR will be uniquely identified by its slug (ie. its filename without the extension), and its filename will have the following format: `YYYYMMDD-adr-title.md`, with `YYYYMMDD` being the date of creation of the file. + +As a result, there won't have conflicts anymore and the files will still be correctly sorted in the IDE thanks to the date. + +Finally, the ADRs will be sorted with these rules (ordered by priority): + +1. By Date field, in the markdown file (if present) +2. By Git creation date (does not follow renames) +3. By file creation date if no versioned yet +4. By slug + +The core library is responsible for sorting. + +## Links + +- Supersedes [20200926-use-the-adr-number-as-its-unique-id](20200926-use-the-adr-number-as-its-unique-id.md) diff --git a/docs/adr/20201026-the-core-api-is-responsible-for-enhancing-the-adr-markdown-body-with-mdx.md b/docs/adr/20201026-the-core-api-is-responsible-for-enhancing-the-adr-markdown-body-with-mdx.md new file mode 100644 index 00000000..672f7641 --- /dev/null +++ b/docs/adr/20201026-the-core-api-is-responsible-for-enhancing-the-adr-markdown-body-with-mdx.md @@ -0,0 +1,33 @@ +# The core API is responsible for enhancing the ADR markdown body with MDX + +- Status: accepted +- Date: 2020-10-26 + +## Context and Problem Statement + +The markdown body of ADRs cannot be used as is, because: + +- Links between ADRs have to be replaced with correct URLs +- Header (status, date, deciders etc...) has to be rendered with specific components + +## Decision Drivers + +- Potential future development of a VSCode extension + +## Considered Options + +- Option 1: the UI is responsible +- Option 2: the core API is responsible (with MDX) + +## Decision Outcome + +Chosen option: "Option 2: the core API is responsible (with MDX)". +Because if we develop the VSCode extension, it is better to add more business logic into the core package, and it is better tested. + +### Positive Consequences + +- The metadata in the header is simply removed + +### Negative Consequences + +- Each UI package will have to implement its own Header component diff --git a/docs/adr/20201103-use-lunr-for-search.md b/docs/adr/20201103-use-lunr-for-search.md new file mode 100644 index 00000000..a1c89fd9 --- /dev/null +++ b/docs/adr/20201103-use-lunr-for-search.md @@ -0,0 +1,47 @@ +# Use Lunr for search + +- Status: accepted +- Date: 2020-11-03 + +## Context and Problem Statement + +We have to provide a search bar to perform full-text search on ADRs. + +## Decision Drivers + +- Works in preview mode AND in the statically built version +- Provides good fuzzy search and stemming capabilities +- Is fast enough to be able to show results while typing +- Does not consume too much CPU and RAM on the client-side, especially for the statically built version + +## Considered Options + +- Option 1: Fuse.js +- Option 2: Lunr.js + +## Decision Outcome + +Chosen option: "Option 2: Lunr.js". + +## Pros and Cons of the Options + +### Option 1: Fuse.js + + + +- Fast indexing +- Slow searching +- Only fuzzy search, no stemming + +### Option 2: Lunr.js + + + +- Slow indexing, but supports index serialization to pre-build them +- Fast searching +- Stemming, multi-language support +- Retrieves the position of the matched tokens + +## Links + +- diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 00000000..c387e977 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,33 @@ +# Architecture Decision Records + +ADRs are automatically published to our Log4brains architecture knowledge base: + +🔗 **** + +Please use this link to browse them. + +## Development + +To preview the knowledge base locally, run: + +```bash +npm run log4brains-preview +# OR +yarn log4brains-preview +``` + +In preview mode, the Hot Reload feature is enabled: any change you make to a markdown file is applied live in the UI. + +To create a new ADR interactively, run: + +```bash +npm run adr new +# OR +yarn adr new +``` + +## More information + +- [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/master#readme) +- [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/master#-what-is-an-adr-and-why-should-you-use-them) +- [ADR GitHub organization](https://adr.github.io/) diff --git a/docs/adr/index.md b/docs/adr/index.md new file mode 100644 index 00000000..bedb18bd --- /dev/null +++ b/docs/adr/index.md @@ -0,0 +1,36 @@ + + +# Architecture knowledge base + +Welcome 👋 to the architecture knowledge base of Log4brains. +You will find here all the Architecture Decision Records (ADR) of the project. + +## Definition and purpose + +> An Architectural Decision (AD) is a software design choice that addresses a functional or non-functional requirement that is architecturally significant. +> An Architectural Decision Record (ADR) captures a single AD, such as often done when writing personal notes or meeting minutes; the collection of ADRs created and maintained in a project constitutes its decision log. + +An ADR is immutable: only its status can change (i.e., become deprecated or superseded). That way, you can become familiar with the whole project history just by reading its decision log in chronological order. +Moreover, maintaining this documentation aims at: + +- 🚀 Improving and speeding up the onboarding of a new team member +- 🔭 Avoiding blind acceptance/reversal of a past decision (cf [Michael Nygard's famous article on ADRs](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions.html)) +- 🤝 Formalizing the decision process of the team + +## Usage + +This website is automatically updated after a change on the `master` branch of the project's Git repository. +In fact, the developers manage this documentation directly with markdown files located next to their code, so it is more convenient for them to keep it up-to-date. +You can browse the ADRs by using the left menu or the search bar. + +The typical workflow of an ADR is the following: + +![ADR workflow](/l4b-static/adr-workflow.png) + +The decision process is entirely collaborative and backed by pull requests. + +## More information + +- [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/master#readme) +- [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/master#-what-is-an-adr-and-why-should-you-use-them) +- [ADR GitHub organization](https://adr.github.io/) diff --git a/docs/adr/template.md b/docs/adr/template.md new file mode 100644 index 00000000..35479fbc --- /dev/null +++ b/docs/adr/template.md @@ -0,0 +1,73 @@ +# [short title of solved problem and solution] + +- Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [xxx](yyyymmdd-xxx.md)] +- Deciders: [list everyone involved in the decision] +- Date: [YYYY-MM-DD when the decision was last updated] +- Tags: [space and/or comma separated list of tags] + +Technical Story: [description | ticket/issue URL] + +## Context and Problem Statement + +[Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] + +## Decision Drivers + +- [driver 1, e.g., a force, facing concern, …] +- [driver 2, e.g., a force, facing concern, …] +- … + +## Considered Options + +- [option 1] +- [option 2] +- [option 3] +- … + +## Decision Outcome + +Chosen option: "[option 1]", because [justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force force | … | comes out best (see below)]. + +### Positive Consequences + +- [e.g., improvement of quality attribute satisfaction, follow-up decisions required, …] +- … + +### Negative Consequences + +- [e.g., compromising quality attribute, follow-up decisions required, …] +- … + +## Pros and Cons of the Options + +### [option 1] + +[example | description | pointer to more information | …] + +- Good, because [argument a] +- Good, because [argument b] +- Bad, because [argument c] +- … + +### [option 2] + +[example | description | pointer to more information | …] + +- Good, because [argument a] +- Good, because [argument b] +- Bad, because [argument c] +- … + +### [option 3] + +[example | description | pointer to more information | …] + +- Good, because [argument a] +- Good, because [argument b] +- Bad, because [argument c] +- … + +## Links + +- [Link type][link to adr] +- … diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..2efd414617532debc76a3b895f6da5c6f2a2afc5 GIT binary patch literal 435446 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≦z)F&hQTJC_b4YgpSSK>JhOk$n2D*d8=F~vq8MO0_F<6A%S12ml#`$vM#gmN}cZ4sN z_+Dwz!?Y^V%Hu-b`JTTW1GWe7;=^$u84y5I*>w5%twi!|Ij>$eW}S>#Ai?#i;GlZ! z8X5DMBcj0qy~Zbo+rlNOyt)TmxE&O*Q65f@3~a5zt^+XNSlAAdtBmem5}6EJ29C~$ zed1s)bIv|8$1K>OzO%zVkg@G#m}d-5hbBM6vQGt}vsRL_h{?GK=v7bj7eeCsz}N(8 z%4eKS*@Sp~X-ZQmiXn>r#x~?_1Dn0Vz9A&<@pD)To@CqYR1tg!w_ke4}8y zYt(d#)1FmAtVpQtNMcpK&jaRx&srFp3xRN%00B4D8yuA(nA8tS*hkBFGm0Kwa9{Am zd}6^ZSjnsGWM(hOt{we?Wj=yeKhd!3OL&;`AjX?hyc7uML3fPg%!9- zf1C~lV#`GRWMSrTmO8S?CA{n;0rBeq_y{TYCkd0n0jg1vjjq@Y01DE9lO6>%;xS6I zamQv(yk&wn%W(^d$Qi~iJBdjjy_Czw&auJzR8TJ4UP+FZ3b3$|MK)$(OUI!nIY1=} z?+oF(<8FDalIPJZq5F}P=LSx^k+1BF`L>?dNsd^m!Bmr_czoox@%BpqOFcGN{Q~Z+ z*!2Pxug6q0Efy04KFU`QodRIj+2|U2{^^n%36|$)%mNe2ap{3KIW(OO3d)>IQ!jvp}gUK@~ zgHy$URPkUE86RJKU4;zmJcXR}Q0|00YROPhr+^>=6{mv0T0*7O6F7T_6%hyFTK9v)F7C4}0N!FF}fXWW@kumLlf$Hy1u z`tWoV-y8FXTNn8}eJ;8BJmeysxc0X&z;sHU9E|*M@Z|#-le8>eySb?UP^VVRRaTEx zfga#n^2t=;6BYd`EdBLCS|yHiPiu9{YrWs7LiBB|f0CfJMxFK%J|(JL^=OLDlRBdT z{=ye^B;I-zTA4fLp;>gjtpuNGVZGyC!8AS{Cy55Pueg7!(e(e_`t={hcn$Es@&^B3 z72^+gSMZOX%+7!IWE%ZD&Ft7mJn(x17^%i*$ABu!?7TA%*h?HA-kaAiYp;qm(N7%R zdrQ}siwdfH`(lsXWl)*Epq<`f>KXZ5{f1I{p-ajBGJC;*o09rhj+F=;tfY&*OY6Gp zbHYnU{WR8{eg21#?2%PpjlN)t1+V`So2~^5ARGXzdrxoB$5Vv%#Qk5ay=Op^YnP~< z-XMkE0)*Zn^sb>A1T=skpg`ymP(x7x5tGnDuOf(GXo8ALQ&CX^(nLf+Ktyy0P((!y z*ieyu;(llLyzhKx&$nmh{C<8UcX`%bo^@Tz;5onIy2^0Aq=CYU-3^m{rBJN?UfMq= z*#714T@|$WADOp;EXzMLZ`b}U^A@S@IHPi=KUXRik(WrXkZuv*%t&(1W>b|wG z;anZdtB#dMN~Ll}?i0e5I8@`}>79e~KioOI3wNc^ufe4gCWFwYkTGuayeZ?a7xGSs>(*_VzHse; z7<~lBS!rGU1P{HrBWSr5*dWM#-93(-s|9QJ%9*#Gm`c@s?I_RORGWOt68?hoa?~?b zRl>sl7MT8*WAK*`h6IBn`Nu_C+P@Eug5W|&oYp#Dkb+aO@03cfy~`3cUTYNqM+0nI zi+^J)a1NN2<>H3z<$7Ds@3jBz{o?WGnU8&i6g+>8&>5oe3~3T2Dy zT}}ux9MaPC<6;>ox8jf@DS~h5+0LH3I=)QP+v%=@59Lbp$gyX|n|aZAreO#pc=3UL zaaEM*mt!~UGu>1sn++{be|_;>KKFM_i&CGTnPOQ-;QOQcqxlc2o1Qtf>~6a>5h5oS zeD>Y;*=g&<7~G*k$z~7pt4pM*f=Nw^Yx^7Ap4xkz$zM4#-{!&Fgu*`0*y+^cH49JJ zGKk%fIATWjNrRgXleG?<9v=JQV^Ll5^v2z6 zsUxLRM>vp}hrB*Y`+MKXDp+dsyXo~+i~^oj8{a=*6ftR)$bX>8cMyyGu+pg5R?jK6~0arf%TUEwu+(hDXtC#jTq^oC|~>eI)*K2mNJ4`O60Y zkiQ_%{JJ}C{Nv8P=)cJ}mjBs-2I!eCW<%8#mvnWsc86zHkGyTTpV9<4=R}k3TCAV)< zD)%yMMRfvGx(L04e-Al83Yzqf#eC*(&Hn!jnWl+^KiFP?zcUgLEHl9X1-xBAXw^M< zQgqA)>!bN%rx!|hHLxdUAJsuDdB9A3;xQTiFa_?_uF_eYs~lcS=jV(k zvC`r_@rs_lZFQk+JFsoY{eols{#+#o#;3$vf?Hz7-^j!l{j(P$|6^d0+H=L1Ywu=$ zVd<)_w<;HAgLSs7Fg-ZVS$ONz7-`YPp%gWZnw4Nu-EgsOs;(ijbP#(=@#q{5*`eZ4 zVUf7>NoiN;aE6Y-g2|is@JGel4ZlOOim8k!I7axa1oN`o%XU}HC1$T>I@al;m*;4qT0;fG9`+fp zMMDb(Ha~l(p%dHw&X+lT!5{k{t+pn!t>g9EdP+2O&Wii0PMlP-vFOdKJh)U~JWujV zdQu#@Ge7di741b&s!OSjm%{aOefc}bel&mMuhx}aPo7iP^9$JpFT}blU#T~3#>y)? zJLX?7&3@AD;gfeZWfi2^Y(y#V286zsDplNeDyiP9u*r|Pr>ysSTj*W6V=cbg#0-Pt zz$~khc5R0w2Zdu*i4YHpOHkdhg81hoqtU4s!|n^V9X$@L_W0Q0rMYqY*&FVq&bP=O zu7MZH{k-eiULsff2d%YVXkGiDV4~ul=oxsw|?i}|Lc9Qn|JN--&^t0F>K{Mvp1kfB~00sLq zWfM{nwNS7>bM70b^Bl58WqjshMN}OB0K%V8RGmvOw$6_GJYEjw;{%qA>7n)a^0r2m zm$^Jrf2g=6KtcHY1Orp2qLujq`EhCZtXpwS`o7kzsS7@Sk~Yo6zxUSvWt=DfS5xD^ zzjPYD_>XPAa?^+a_eP?T1~& zVg_4Y5KHu)o;YUS_?{H^PQjGonz)kq3&E7Nq+~CqU2AkuFz|JY8z`unHGd|oa_LIf zsvFZHt|1ZVJZETO+-hsRw?S|~syUh|u(RRnyjC(#oFlxcA+~4?r5)vrI+ zkB+5umDC=%lB!p0$IR~IpO%WC(~_Og0Pjz^Qvm83@urkdY5qg(6a zfU?#-`^%v^j@qS!P^EI)=sv$II$IYmnefw^FVfqTnXF!KIQFguy_4i~S)tP?BWb3q zEp1zlxSFf8dUIOG-DC3TgOhK^UN0*&Gn;+#{X?D7Ta97=q|*u&H19vth=1$k`yUuL zBnmz(SS*`njtLe^l2FXp!X(l2V&%zSi)B}a!8ly^O`zR;7;WYhpp%Bb(@J+I!*1dPCe!74AY>ipMF5=6=DYWwO3$kNvDo!XJ zpM+23m{<-*-kdo7ZaFr7Fs;-f>+n9@ynV*MYz<=m9J7(XF=qc4VZ-6f?xI~gYOebS z3WN;+XioEiE4cm;^AQ2iP32b0uGPcZ4$*zy@Y?1dbRYZtrqMOaV0;vv^!`?RC|p?1 z%GrI#&oNa66FKVxpQxa|`P&6vUVY8bUq{de*z5Gf$Md`J7TVN>@+)&^JC*m`Ca#7K z;pANtByU@I6BaV85DqIV69SimEd%hn*xxj|L`YaH&WJ@G#I z#J6cbGX+Nrh_#f4!M(PHC@1CV_f!5=6m(aHh1uM-toY|5u{;3CTeUZH#_6ADFk8?U z{|LsM|4}fO+MXI43O`swRM7t@LuO{kw+Z}%pMy&t0fclv+CSm7_2m@Q$+h;CHimBV zE|sZ&QwmMXBDLrg?zs3}Lc9E8nB}P{DHPfDBrK%6bxn0}&k5oEGn~XDFPyK?f0tdc z>mml1L$TU!fA3!AF9UdypqYQUw*Qv_{Ex$3ezu0**-2j|X-f-)d(CNNoBDMN1M2}t zEl&=pDmD)DAd--kuiQ8?wP?2QUP=A0dt2q{!d~|L)d(Fr!kMd~g<=afb>-}W8mMebSaoh3fW)D~VnPU#b17eLz`ll7W?$5w*|8E3_|4A^fNBa8Mc;ij9CQR|c zfp?4Whk>>U?rZ%0Vmlr@clJIA`N!p6npyYwV|H8Us5g3q3quzaDr`-lEeS6=3HKLG zDQpm;6g$YJG_+@{pNO!WoI+MaaUJbe@>@;snK=ZTIn@?~I#zoZR;1us-aroS&nb|5 zdQ6qoB`f9m2>!?rS^E_azqVqZH#FQDzgXcsGC)?x)@Wb1LP3L>7(;UK7J@Ua@`3sqv z&xr)mjnw{|i|*!>fT(ke9ggoD&KczFx^NhZl8zl5o-z5R^w5Ulz|bUSTrTFZoszl* z5cE=^xOxleRy0qrQ?NcT*IH}6Fguv;an?jDjzA^YCgXQ1l!W%ak_G>-1O~sd?w*4F ze>?^M+36nEC$H&s-sC?!-A8xbi_aSUXQ%s)mFu2=INkr@8_jd`3jF1Ce|#C@UcnX^ z(uMKh_!&0UK5HjfeI_NFla{rsOg&zj%{6B+uWel%y{JPu7<7lREuvrOYMX#)KU6~$ zS96!c(V({hDnp>+Yk)C|<(SHO5jC5bik4W_{Ag`6(O1H@H#$kvmmYD;<>L%L3<-Rr zuV#D4L}c17?27%dr!HFsoS=v>;zQ-0nIDkm*;REuxVI0o*?2Avl8(NK+|zI!)Vu8- z-r2d6WVc{Ld0%%+s$lD#Ey?+owBk&t+X{|J*w4l7@wscd%uCzxnn4m*;PQ9F<+)sk zc#&IFhaf>! z1qPcmd|}8bxV@4~r#Ndhx?2kqoRUDn(oYeQ=^A7SIh3Q5L}nOp0Qg=lEDb~}qB8<4 z@xf$S4IP;VeEFuE&FA z19XdEP)X%5ma69%PnKoH!}Z9d``9tjo9ET~8c$Nce*#HdECASJth*`TLUmMnx{}0q z2KbUy%OO~$UlAWHd}p5LAmJyy)(E~gU>Q*xeELe%h0Cezfg0AQ*LM4EgQ{jK!fU>< zM5iK{7YHhvi4=?_1P2vWP+evC2^m?uxZ#XvazlU{H4AEK$9Io%EnH^#M)B!X->$f&XuAR3WC zasma*hB-jVs^iRYpg{!xb#qgyXz$A>ytMKyV-zO*{n!wHNCjD@njdKd^tY07$j(+=|R2;OofI6O_XVHZ0>Y_w)<4*ZM^k-g*2q_3U3Rm>6Hl>(9@@`G+Pl&14I--CtLKvH z`~*(-!R32EAr7nGGae?&k#dRmh>|5!nUd)};O_Qj#|{c3N!AWNaDycN=4kU+igzQ1 z*LWh`xq7+g2oR+QaA@T6oMZ_mCL}wxYkP40Fe=2mnIMa0fp>cJ096nUs6dXnD5JtY z)sBsw{SheJchXdP@LCx7#d`8ixU*BWqrq5XKf2QsogX+T5E4 zV1?%e9;1eIZkhvJEhM{$pP}4!S-2ipI+a9W~e1 zn5YB>%!677m}IpPpG!g#e=_!it1|#plv4T%3AyxEvQw7*(gpvGgPw!{oGaM6k$O_=-#4g-A6#!;9Guv+Z>@C2^Jx(;f zZz!;;4|@NY-REP1lGbBjsmuhcvG+TjwlfasOwm~AS2a?j^oBW-F0%QV)$4_{O}`I` zFO3=fUeZi!zg>jEjzIqAE{V179H#|5oHYO>q+LMvL8P36fxDlt^`$gJpXcglDN~ra z@u%zM)z+4+s{6DqEV;1v5eii@c3bB-v@7 zGhDjX^ZVw<-xSc(sRG~VqJ$lQVoL-hcua0p@sarP>iFihXvi<$DA9%>nX*dr>XSs1 zC9nwQhK7rTz&C0^L}@yGlfn~bjw}2An{QO8jisddhi}xEWm19FtN6n=N)EK1NwfRI zHwrNpWV`5oc8#hiQcs{c}n=+VMGmg#?3^2@umDF0rGs7!sl&OV7x zS_fe-+L?>~8$IG)y1?ymo>12emcN}SxF4O(H$^=0t6O~MjJH(JZ#O*jLg;z|c8e2I zJgd;)aJ#HcZ2oWR%1AHjjln!**iF0m7rYbi{bWWE=ulMTMc05avDg`$W1c5>+GR}Wd_6zF7G#9DoF%Ea`dPB@mHEPQ72Sl_E`HuAx?Ak4&Oo98-EW@|!F z>gL?-M9|#r7`O6niYL0p_V6U`ig%n|(3YfGXFp~&#W*sQ&X1HfdOhZl72@8ABb%-a zh3?P|_%ucFzBAOMAU#O%d@NLgScI}RlvL%k+FsxwF44tn_oZkugf!v3G)|BF>2xk6 z4*6ir-S%a7>CUZH8)GG)6B`|9tcEP3dTUEwBn3*w$4`q|Q2Xx6E%NIh5(4_h94+3L z-7P3D^{oxHovrYCxRuL5R2{GG558MvK>!NrgrATYzc4eGM3W2hP&9UUmh4E4*U0p77%@Go)%bd%-w~H zd)*SKkQP)gpw%!hDPY}23$KSnpG2(!o3uo`JnOe*wJKZFx&imMzK~u`OW##prJq_> zarA?_D|ey5)Tfo+V)HpgOgl)oECgfoVPhd9IR^I;9se;b9xAd8f8l+*fjBV6c0u^9 z1184gW1vOsfK5kh;3o#r9#ymV;Z>i`T!5kaBz=c6sV^<(WV3XTy_Tc@RC9qtcl-(T6Z-}mZ9^kuiun9%{7gpJrC zl_q+xgl)sv$kvDJV?|NS8T)NiEvf|~s91K<3FG9rCS=$*8P}g3J7qs&Pzo^Y8$@6*j-c%CJF&37>YGxp{i^HC_ds;k3yzWQq7=F5)+@z@rqy>5 zZoc(ULz)B33MXqn#xj{$#YoI9%ET^*f)$4#Gq(GLe&Yt)ejiugxI&T*n;eii_6sIa zQT)=p&2|(Bm((09#J=FlZP*p+knFP8$$fGx5A@NB{o3s^Jabec2}2D%jwfamCvu!o z?~6rc_LB7<*^$8h!Ffx7$*umu(zaJu^Ue4`5jKUb7G)nN?6Rmu9zStT_q>PMnBA*k z4hN-_DH_PZ>#7?HO7a{yD7>i!l3U?tco0KzZ#;Sp$iZ-HVHPIdvscgAgJQk+J(LNk zkterRVIlU?;Y60CCnr7h6Vm`Cp(qT^w_IX&D5DN$DxLA&myol&)fo4_ z6Ko?=29iX8#Se)L~w44^o-aFut-ovpY8H0R8Lby ze$JmIMLDl=zMFI)A(Vw}X@$h>XF>ak6GEOvmWM_hL;?^Xj0GnzWfFvPU1muPpDM!g zf)%E_8lJ2)VzwvroOFw_Gp8NOPiRFVcGx3=exiJPB36ULaZXmfY&}XhPy9Dvnk-KV z)3Spl|DC*<4kET;_wNNFd<6EV9KJy7E`f>^Ffn^?dJ`cFG+pgX>pT&-8s@7|`i!>X zTs!W@EEf@^nNcjvBdfWjDV}xGv-($zg}250#wS3U=m-L(!#2xPPXtQ#2459x*ydT7o)Q zK{yzhDcdi4NiNra?8QD?kQ&YV$#Ja4r%#k1YguoEK&S~ zgZUU8#rb%1$WICOSn`zyaN4Yu9G4{$xh|-Rbx!dIY0z+m5|PO#RK%QT=x-JeMR-hsC-#eFASF8JJ%Rn#Bd94Z#RuDq|YrvWV=}<&SRU!7XDq2LVO;VoZQ-#MRVkC8%j(y^jxD)=;rTlT zcV~@SB_5r2859m~S%I|DGbCmKLSN!c-gsTU!464A5g-C7+CTW2a|o+#t}%|nSE;*A zHIrXF|Dpc55B<&3?R{d zr^LZBsi?!oKXxBoqaPJAOfy}EnzPZCzERh&q45mlX9lL8c)XYrQgRoxXAB-*6o&Qy z8y1B;Y|_+&f!#HD>cwj zufsEGzCwL*I8y?ojf5({2Hrvd2T~yewUHJq+>S}m4N96W1O64D6TpKn;#_?vuotQ3 zZmmX+0N6I-?mIlpQvh7H0e4-)X)6`t%t_}=lXWSM52vASMFKtmuEhsQ)*pS===iw^ z{eX-9y%m%2H9C+Z^qUm?T7~>Yfm9(9FmF-tvJ34ej(!gi4um*|G%zj9v2OsdZVPPv z7_@zU`}`N|kxz$`24K$Iyio*PgPb=V3*FrcJc!dL?>hLv6w^l&>5L5_Zo5 zv!?Do6&N!QklN1P(Q=ql4K^iR~iUaa!3| zI8Z?yo%UA6;dHGl9Gjcta_p~_nfFTa?`D=!gtP^+u`UJuMvZGK!lm320VvcPtOD-A z{m#P7b5qb>kbb)2%Q4IsE?a^wR1|@^sVdBgNAtM?o(A&`fDRX6i<^AdN5FC03Ha^E zcoGk6hy(xPix|K7qCCUuDWv6eEiN=T@Hs_*1Hx#Q$n6A7@EYa=(@ixiexntmau=sd zsGfXHdPsnPO4YcP#WQi($77fc0NIcvobclGXNHG}3+6=x?pi#09_KQhU5)3S=6^+h zB?#wup%3+;scXV?dfkKTt^#Dq#fUb;m+z;eH%S<96YkoVfS<%#iD8I>MPc>T)274N zyG3AIM*MpbS99*ci6hvT6oJMBJj%nYaqQm#2y#>uRJG1ov`%H1@VK&isvi{ay|DyF zF#Wb~>#Wz0k~+CrwiyFCVI*uFhC3C!(|=4^prQdKtV4?dHsNGMacg)w0A1B8nQet) zQ{m}c<@qp36IJLCO(-WaEIAQJIzX;bX$~b%C*i0;wZj+G(MCW*ldmftnw~zZyf#tk+juvHdqpiF%^$mk|rQ zM5uQL1k+h$T`p*jhq=OqPw_B6qR__}p83;=r@k2BN;gyw@ORl(un-*j6m*S?`Aoph z5sGHourCSN6~HDsn<1q^K}IB)v;*LG%nvq+fS0W;NBZc?zecqpnwl5YS4B(r|=GXCtYzoeMEIiwC+~ zrDj9l#bT$40eGI#GD*k>uq`vBdaV`1rCY!D$J*Y%G+zcjl&gEB>RR}9w6kCWT6Zi_ zI{I0V2?q8QKMpx=bQxeewWw9X7aHMOF`ynE*Q_{vC5s}^e%zruoPCoLo;7vK{*gj4 z@biWH07P76a=T=|msLz8*o3i2`$f$l~$Yd z^Nl}*KC3C2I0?Pe{&$dxj=w^~S@3~c0LZ0BIz9s36R}_m4zhn-G8$N5&H)PuC1N;8 zeGGiX>&~|!hv-$1S@|^qx%A^`N;uKih9je11cJ}$X>kKymS1x?VywmC(j|ZCq$nha zfH|_|gI%f6fF3UW3&@Bc=E@@{s7Xj|+=k(Lthl$k!lCMpdETe4nGc_V+cuoA}QP>^*9=*P6h9uKgr!wwsS!0 zR4n8w5y9~Tr+DZ#<^G2(kwOc$#-kla{F_JVV zO2U|ghLTZnyV3C`Cae~`wDK_MUcmUHht)n0&3SB8>}cuhhu>Ck4#TQHV;(wuZNhj+ z1+K__n;G?F64!Od;l(>_EANOgA4aHI^*Bu225vq?c}UsNN03Sqnh!+8_2j5jcq0iV z5G&$6m3A&Ya10h)AdG9rf}ZrC5a>xEyUE$uNwTfjv>POyhzNQ-sbuF_Z;ZNTIwhto z6|j0J&2Fl^y=RN?^k{F7z5BG5;mGCOX}m`>`HQU8#gS+|oH`w%LIiCd#mD5!M%Ugy z>dsC5`Pcy1y`nQCS1VIrmr)S|&bVcliGz9o5J=pk%z!85$DUN&5*J}WOei215{Ss1 zthAkN2$(%$F8+tHp1rU6g0Ll~zGIKqVn}&b`MNkMuO`Z-ctDc*C_Uw7SxCkF8JvOUJ1rczm zur`3_7|@U3^CUn@gbt$-5Z)vp_s6sK>fgR!6gy@K@ZinuDgp#mJU_qyX!4;VV?bdN z#F7r&#|J>ifG%`M>#g4;Q|F}>L;*zb=>BID)z3Y6096VURP@Z04#Cy{kT}3Tz}(a7 zd7{FiQOylq3aI57ur?njgoA1jLDSXGcEg{26b9@mg2D-l12sti=!HcO zB1nV;lIQR$(?A#w*a86XroM1hSPChaH*W>dcyq!8fWA9VfdCdNf@yLAx*UiL1#qYq zKqEo7ZCTn(5tRN809%d$J;!E2MF58mzyZPXPI{rn1*j7sqB}q;6sQLeh-6$(t$BSa z@QDEcHW~ne@_VurpxXc-fiNmoKNH9Y6g>kX2ydzs-nIumiFiLJPXK&x#)%XG6gNQX z0;Uj9pojwkI1AnSvmG@n!x*_tCeV{R3!($6;&8I65}#6JBfc!$zx{4bL0o+dV#1w; zQlBqBd$*MOKI{Q7gZfsSm$kh4o{yPSp@T3yzC5_SPz+)yCUw<{4$*{(<2oWJR|x+^}2{qhZSFTrRv~f*Tjjgno$HwgaVYLrG8Xn2 zAoLao&v2p1*rS#NdqJv!gb{L*jwLsIAsqvgi5T|_g%-!50b@q47tp#T%5z-*UaBm* z7LY+Tn?T=J=wS8KD>%_nN%iPjg5_I}UqU)v1T=pqR!}6Bx7o^vQ8njUJ9}FprW}a( z+l^XJa1htHvlrr00&3=??|(&q55~0wwO{&8S;-qRL8{##Z5u7}yz{%^8~{~5gNvb~ zG4{eAs2G4DY6z<~{^p7Cw&D9l;?1|)I==2NJ0S-{DY$jlA z-hKamNe*o9?GX=0f9uqZm6!1_Cp+S??>v{zh4vTs%5?VJcKlxJdBxz9*(%SlUQW-# zl-p-5S%2@QuUR4kY2GrDt8$0X{%Bwu-1F-GXSXxtcE8_y`}hxSO*fa1vxIf5G3I(9da>rEL7QogJo|6-Fdb#YoX;-h*hPVM1z1NnPi zz$X3vDXBekmzIUU9PlNC{0xJR|H*Esq&qd3v6DNti-3jY4nD8I#okkrwxSg0e5;?l z3lH(nk#V>_=ff^i&s-XB56#*kGbu>idh<;CeN}96>Xh6|>4Z+kfDYDu_{ab;kyp6bCpNMOE zE;yX=x>q)Wu^bGGVPr36NLOfWL#i#vz%u3hmj@~=D4~(!j)>xZA;S7XyQ{$+h_Rbt zf47~5AFnI8^`P)gHT;(Hq8=2XoVE#)^DpNs!D-WmtnB)t;h2t5VKPuSJ~bG`Rynnr zS)kr)3Coc#;|x~F#^E5Rx3si|yU%rN-G!+%j}PQ3j&!ov3LM|MOsT+&^X%=>oI|^! zF5TCcrUZ3GNrO{(FHf0g`LbrNZMi_rJz8T#MOxjo9LT}v4XIb@loQ)7l5VttvB$m> z(yQ%mkd-pzlE*;!(BZKIj{_fT3kr8W-VgHEQNP$piQORsV#~7q;01QY_C9_Q)z9ud zOJt0n3_1>!w77M3lnb-;v^kM8Bo$@hgeP)&!*D%na8`+tc30zRJ9dV0vtdxCWkhA< z*(ZQI0I|H*r>*Qv#f6GZo*jJi%RF4+=o`CNSBqy+WgVZ)5*-wMVwUZIUVN9Z+*n^` z=tSb-B)jZ5a_rTsPY)|=VEm&+Ub!1D55 zblQI5)X`>OI5PXUS*p;v{Y6h1<~Qc8vScayj0ts>%ntbgVAU2r2hDjb$#T(-t5JWARAMl zkovsvVfv!`Mv{}t=7LzJ9kFNUfqk7kWJ@+Hb)x3vUEUD@n%0-jj_?w0P2v_isSN%g|^$qm?KK3M(}DHAuD z_h|2CJ5zmZQ6{jMVKgcNpO>zWcAt3$-+?=`j$4#dcdebxqoChiZs2(5xp4pM(Gx?dDcXqcYjAm(E7_9TpD#`5hDKCSk0UH$L_ z8!TVFB4?Rrl_ym2Q3wYHO{U6k^qGjU!dNR4IC`SFi|2JF(k648o%V!6(WIV0U?IJj zb}wEPwB1p=%(C5c#Mf3=*g-rQ56Kj9H$y7HFZJ(X7^&Yp=lU|O&d^J3Q z9&--T>oPIHpAk274l})L--Z}-MVxi6Vj4Hg^!DE!w<@bN!`|u2G9306N=!K<+92I0 zy0G?SY)6Q=EB%$eFAlsQD|LPN(0w>c1Z!oX1ggOAgwpK&y6u$`W2VaCL ze&pT7?w}w)o=_2Y=bM;M!$JucQryQ-MNOYhuz4m2jUI4pM9` zI3x2qvYWci_B~Aa@y}xVl1(N$jvjHSSq2}o$zUt3p&t)j2364Lw!`zb2szU5uB&my z&+uT$GL=!-dc2_CsK_-sN$UO!CfbYvO$O9Mhby+(-d@f9#xa-N%J|ecPY1bG+#P+W z2`>9=9qs?>tGD@jPh<yveOU9%S4m{jE#q}FCvP0pv>qIzw@xIxud0}Bq zMy>*Yi1+Wk08x>SJ>mmbXE39a7~s!0zIguxncKVbfsmW$p9?AxcQSFSkS0!$|9F<} zz{ZN=Cs+9Bu-TsVcONW+o9BFZPn{my$5mRt)q82L+5KXFMt1#PIE7)44xvJ2Vzizk zj@h9S0qGbeaajAMLz9h{ChHqE^vpFA_HdL>$~uOKrw^qW@y8JtxK~dZ0LlS?B~l_$5z4>jTKan z&3_npzhe8_JvzVrXz3rb#A9TY2?0ZQ6R}jTyD1v;Ga4AvWX*} zFLY-`jC7ksKIi@~{?}+gh+h9PH7f&D@xFgOk!**F7W}EYk2XJ>6ucv9DzNn&@pH z*l{RxPmi^uCo5|GvcoD!ophyp4Cp|EN42sPOLDD8?%Lu@Srx>j=1g}ga--NzwzFqr zO-TB?L@zB5*B~Un3U{AjZ0EyifKxYla(Cu|keFNxmi;NMvS>!eAvXB)h1{Nfxp6WO zy?u9#*K>`?JvS{Bd>Fu?w`1 z2%WRYv}317ATD38KO@Hj80CQX?REU2RT|5cF*VFnAyg&j<|6;lL@ zOUx7_WxiPlDt95$DQu-pMAa;VorKHWv9}jbg2b+8=o5tYP_qtG@S&|h#YCnj-Sj{O zMBSGal&JP?o~eS!@?e0nir|TwSsp~mE(+=_9o0<84dP|~u1vn12(+ol?7+d67m#NF zIkM|4IvJkYirHeripD9VRuH3#&eW2SClFn-g!Bk<#}Q3(B?no>MQT!+v{qP!7bqT= z+2jTCWxE_2xhG2j?I+68IT`wVR9c@z*BI&$J422S^k>0zXxk1X4kn7Ds`~D!QCM`E zYFw+vNz$Ono^6`aBfXtXZ_f?oo`&p*W7}4-?ZrgfcoOPJt+Tp0O?kuT#51$=Mv}SY zNTl3mMVU+R5Kztnl+}_IEe+R7y4OL@l@%VWxL(PX&g-?v>+{VUNX#3m$Qx$d5b1*X z`@&7hqlY*SC_5Z9xTr1!9>Rv$&p_OLb?%&V$yfpDP=UVm3qf5MY>@fmn)%rl83)He z>g$5J1B#h}SdcQK+393XW)cVT@LsX}>taPeXI*Mmh-QJ$GFZ74=pYT>pIA(uNjIVu zzh{f6R2IBpyUOLJAI4qS!T~Ew7s`r=h`KOI(zc!z=@bq`JQllNggxw$xj(Vva|a!8INb8cvx`+#D09vPit;H19=@Het=e{w1#@MA)VTEOuRBBOKYIVKT z=1J)m88t-`i%xL}p=NscY77#k5=GiP4!+2$mv1sEsX@(^g^nn)OAd^~+M$WbMDjI`9sN^D>a#E_V|w8LU-a zc1)rqAFTIj%F(aPIhK^Mdg4N9PB0^_Z4$nnCqbu6CuM+>uRLY~+D_Y-DYE^PQ>G}~ zX-K_VFae})!ECK4Df6pTqg9gC8lR0+_A)>(#&{I9XT!WXWDN(H+zs{xX>Kz+$o76e?mbcZvA#3Jc()0HD6(?u!X~4_# z2Bg&RG}##$W5D%Vxtl#zgKHjLUBHXdp3PiBi_%jy2U#K|ZFIkV3s^Zo(=So*Eh!j3 zn1qBDJ%22*_k5dd3$pH+9mO`zD~7cFC<4x9D7^LyXo(Ft+01^gwzp;9nQru{D>Dom z4xxE@z-ZQFe+7I%#`xkb#h>$vzXLfc+8nTK7Q~+e`)=0&kUuca(nqCV4Q3fALk-uQ zyH8{^#UpR;gB_s35-E>!y6{|fJ@Pz9=sct)smx;?7Ce;c-<%b=MsHX2o|L%=qo80CFJTavecDGVt+ z4H>nWyS+BD?MnCwrOZQ_3ao{z2+!R*~gE zeUypMp9klhlDsbqSIz{=eQ~8P)O5B>211W%cr9-ja5QDUQ%pgX`idpRvv0;nbn?H({2^j@s$*|qnGN>s8L7tteYrJSb>WE{S;2ID z1wQlgb6wGMh!3z26n4SY8--e`D-X-BkiP?Ly5JOI$s7N7FQVQ(F4+J1>?Lj{yjLOo z;dbWL?-?${^k=NJuQ`{$O=NxVjru-s@H>cTfg^&9TZ2eMXebf>G=21jf(uTUqdF(-2)d*aa#knJ$UKQdJK*p~fbP3MtH!2wnj1)eyjWko0#XOITzkX%!eP)1|a1hw6MczzK z(@bZ~8JP(b16cJ?3^+%x9$)O*5ocBLf0%o(sHVEM-+QgBG)P;acR~?CK|>2w2)%0n zL9hm}0HOv#L`6-gp@*U%poS(`(10k2sG%bw21G?f?;sXb)L<{jz1RIb&-?CojPGQR zZ|sA8n1f_wBr`K}UcY}CmGEeTit$ml`PYpjyX<}~CWGDHWQjC4_d>=)2JgDzxxxNAEmT zg-z`5+PNOMmT-udxWQ)UQBOsQ_cpgW4GFK|GB2`OxDIU75&k$~kSyn7SL_WItkc(J zt=uwXGv{l6`qHhu+^aDgP>EUBG*~|v+kR!eE29aLbB-R%#kFqL#*l%5vL_cfa!QI@>{_xJl0TBXB#Sw0n5N-!|_7<04O{3p-yN>W+ z+wFuqX`2T=zjjKI>-g9cmwTN-D$&SWXcHf*oBe&Q(M7ZI`XZpU1{h28H^SI&9WZR!71AnF-{dxNOPko1t&fqQh2=9wu;e0Ko zwD3m7rM~i_Tjg2FR|j-wycTtK1-!m=>)F7Oam``*JDF}{-EtSiTMll;=dB`+GiP7t zQcb%QH!jv|9Iay=Rdnx~YxsL*my7m`F6(UeiUT0S7V-q~w%C7s2-&*U`m3;^c+V9D zq*R5aK^LOupL;0wy_GXiK+lN($(jITKD`iYxQb~*p8467Qi~m_aX;vhL&wav^w(uK zW7U$b9tV7Q{+cH>ZBi_Dz5QY`-Wb#n)YM*Ixt!+|$NRj9%E%hmvbCK(h)-hYC?NvG z@MtEl$2O!^zi4~hQ)MHgdx<+k`rmKNvD@33JZ-8;)#o*IA2yEXO!s7;zv)f&J9%o_?7g(qs@FHf~KU|iVr;oJTx5#?HQ~YzxKq1PV=8X z7-~26eG`wiC&*c>FG94i$TTi&NvmxH`kw=s5u zC~5#+=^a$y_-g!lpTh3|E45`(ad07Hu>>*h5PTPEv>iX=OVUUlTIJGIKe6)D*jI$ooEra3GN0pH`uZq8VD(T(>9rV}-o%KEy!vZDjjG}cl8(hBckT7IK!KxsQ!pY<~!so<4H$D=+Sfdri!8I}f=?!UR*%i{!cXK2eD^8&1$~7Fp&g z+wx7&7LWWDvXQ6lU9zY$2P?|yHGP?OBag1$U{-!S@|;OSzsnmvK99V9wpzDSPqtE) zm%B#amhAwO<<+{$)?bWM-D>s@Ia=Pej{0)W^i=b!^{Q>@Jp&3BVPLO90Nd@30@3Yq z0oA|CUsBVT|1*_3-nll0{;V{@?ZIU7d{0A5ST(HlO!*t?#1}OvbRz0IkfE%s^t{l> zgiXD-+;M>&D8HlEl0RkmZf9VGJ}=1g@k(yQ$V&eOj=SYE$$Zc4$xnMp5{D1bU(7$K z7X>iZCGT%8#jfQLZtJqEN0u+=B@DsN}xz5&7*Juu6m&KE9IRN3yL@(Za?=8ifF1k%` zc_@FEmWDS3xkv3dYi2WAL=;(C4}!}OIzx_#qsh-K{b=SvS%ojbR<2ii|rO?W!~MD3jC3OUdV-c1J1+Iq8d0 znY44dh6X0L8#;%SfehMUGHID^{H`-c)5kwiv5Tcx!vc z_!B69dPtrf#w8}?xsr;QS$H45awOHz?3QtsLbADA9*3#EAoX&X?4m@szSOsu5|$5f zx|zFEaoaASHNJ;Zbvn5vr_e7Io{80M9jS%21~W`2KqtCYs?jsa{k}PH$a}O)p5Llw zO+N#0&QJ-z1Q_B->YBreihT5yZ0#63kFmoqur-GDCb;>2PhyL}Y+;Avfi9AJZcK8qPN4nYmPQ|NKKC{sI53(l+1V_!={n=VEsUV=ybK57MAsbD@+A z+@@SiG#zOuT9eOAKQwzx9lerK+LFGE@U! zi({+FLQ+9}|Ei0I%f-2eI#JZC`3?{j;(oSL#84zu|1Wvc#HVTEIZpms|rw`V|(ciiI~|^ z09X7YJS`cPOCr`ds>blFo;zd*u05mj30`L|ozAfUvh)w1+`TWrDsS%iI<=W|A1Tn?e4`vW{5CR3X-G5E_ge! zyY24Yb_-e|rsHR1$`)^dyj6t8*LIv$o($5J3NeF?IESPmFtEHQcS=TB=#1q?+0AHw z;||zwtyT@xlw#M-Wm$I!s&j~E)OIcI{@TEuFUws^qr9BJ)#WqO6*(O`FIkZ{qc^1m zZS2wcM$aFq4Hy{FOjQ=zxbc%~7T?C9;x6TEKVz%7H57@ko zAb$!2hpHz`$#Kj$wD#}h$@ht=EjQl#J&I~D_3o;E*G%?ZE6mDQoAxyO#B;>Q;hA`S zC$i7${Di51jB74=3G58wVn7sHBVJcv_M@SmolmrK8m0;;#p3o}OoK z4er?5;*#wcFhpZKy*ATa3DGfcYgs&ozfn z(e9PuDJZ;F{l?Q@pZ)Fh!>tE$MR%wrCcA#_aV|>==yCfT=)5qq^6#H%Xq>J$?^zb} zZ!z)AYe(a^ix?q5mi8-q-V0F#Jw4JuaG`E3%87<{@?Zl02dQuW7o9@?Z@v@=Z~!@! z&A+;CNJw#y=>O9()Lhf|FW0>-C(El)%PmLg$NY>pv}AwP#{YKRtD7(JuCR4D<9N;T zKRxokO7(Y(Jp)u8M3?NiSI_#VNB-;Uk=ixqUE}OOH9ow={-;O&^RkDRm)$dRzjLhV z$pyoJnPX%1R${MR*l~7>zo%3$0ab57$fX?># zhI71zReu;)g8sT?@ZEEHmBpi(iOV}LJq|vw{aM_}#Wihl5{IvEbK1^txp(tIZ(EY1ooY3eX>-$froi!=_XEBY*5rl2 zit72I$lFN0xCm`RUCr1e6Zfoqpf(Rte7B#iFZP$Wf*5}fqS7f>qodG@_Hk!lRX8R6 zthae5X+|Ggw;*#hc{Sd1*U-(y!QhyACRHmwqp*(5#~du&IJ#cHz>y5&9N&?$Jf zzC6|jIrHVAB(dG&UN^}j>fEPQJBv8_g77fp2%x0bx$a?(X?{8XUUL> zc^0pGr=d1k|Mj(HRY!h_}VU`a(0F?whr0vm@suO%p{t-pVl5QP4L`QJYGby|03==q&hP9 z`5qM|_eJ8q$1ri!d#PEGoL3&*buea`+1GLMdlpYG&>i$f1qQ$W`UvB}MU$2ox}b;* z{&tk+7I&fBRi&3~H?+|#i|%PFWkZX^zLz&_Tz0lSg(lwplywu-wz6F1JZ0J%Kr|~} zELFJyWYSdN`2C(+$~hZfV?3Pfrr*7kAxwJ*GXp499Z%Ks8f5p7L)J!oe?`3^&ZOZw zbBpAB>s&lK1b)x#Jvsiu$O z0SLoUEOeI=U!F%{UhUwjvOrR%x0|LG>;!4)5e(bKXuUFwl3)*72mhDcBs{&=s>2s2Xm)J~1mL07M&${~8gbXF1VGDzgvw7)J}P6`h@hS3{~ZMLpm~8bK+U2g6c8 zi4O3Vy1U`F@>5Nd#aP?uSvP}UZE53TfRHS9Fp~llE*G;S%*1D31+48zlgGCMB=1Sk z2gSxxZZH)zaM#YN2+ys1*|q6&$GXyUrS%Jzso*&nlWSCc3jZ@UY+(JvrM}0`g?j^( z_D5mpR;88*_a3~00F#}1C0mcqBoX;;i!a%@*IO6s!Abf$w2~OLF-_BX&E9o?wpAbH zr0r6wiiULEM{!n-t^lI(BYj|l8ORtAl7Lv@o85#jjggy$n06xo0O4n#a3_*~4*<$9 zc_eRcX7UQ9y+r!T7NaM2x9W&2vnf0C$7DHRJvc2W+wcO?`bqoSKTS?w1}NEn zuG;=o2V%i#THOiggC6>e1k@;qGo%4;?$7S_NKI1`3DG%~$a{P14th~>kdP-^{kEvh zsJb-97?e+Bv(xy2*HJ#8%8&hNzjx^oaLMPx-c>gJC(+y{5nC<)5An}KswRCva$4i8Kk{wDEDb7Aes$) zcNjFS1EC<`#n;45fzX6-^3g~Vq(j3P?yUe!0XI4HNo4v1-fvfAH1FrE(wpl5OjtJ8 zpOW^Z%cLk(wN&!KNn3gv^TwIgT~w8_Ok<%KJanE(I5?3>l`%&WROGzAmPHsO2DEQK z{OHi40h{+BB;wYdeN3S0>QrLH{ydx8-y8OEAYd(cAF&DD2mmsT4fP7PDyGYMpM-)d z6WVFBlepp6T$M0k4SwoW@bZnpqv}GuO~^_>UxF(6X!DE>RL|^|g&KCWK*o|ME*z$t zqWcEGs+A|#LYt&^S`%^Y&mQcFMe9nQ#_Ye>s+72SH09od7y_P)n>w>0bueWDb++Th z#m&k4RDt=N9i1+*>~ur-uHx|DI>fuN=~`;6>4Z&8<;mgbdrO2bGS#AIEN>QRngXO> zxgVc=o5QO6{XyYV9^v%4Ty0D0$CX?#%Q;*ipIDlgaX z{SAqkIAhtS(Q67AvFb&|>)4)8H@TRn&*I3EqtG$$MPOo1Ao2TtOlT{l67R(-WVW$5wc093H(PdXAoqGzZy0>alk-f z=E=*uzHdi6FfrcrOe=xUzC^g`0cgMmx3B|j+Ok4qF_(ks4w9gi;s_&{X(U4#OF$Jm zYM^YtR(bZ7hug`b+?1`LB@1H{lDotM^H&!7cYPYlJf}W6`$kxx5gSbupoeCGVSEU# zi#hyt6Gg&&{~9oqc#vPj1lr~vI_4C|&Vo3BXlY(>81;Cq^T+W#%si+p0j*$+6>Ia} zgg~b(6a_#HeFvjn6^6sW_5CoRb$56PI-D5{$U*}pAYQcPxxqRsrZ0x(0Cll&>5wuz z@6W_`oXEumkIJbwUn(vPFIkLOghH(ZX!Fn`C0YA0Y`{(u1ltG|jWt&*HIS5T|?5e=V80Gh@yJhy)hw-8Wg}oLI|rM!Ll+gSt-MSfRk2IC=x7PE_&`nm z8R$TX5se20?S=C)Lzeo&2NEE&^%2KZR8jp-!mS;SHj{OM2MvhT=7`lMCP%^3-gX~@6=NH$ zhkI40ccOmP{c6AaXe*)Z(?`eGN1&3~KI_~;9PI=*^6c@g_}E_Sf*}Jf7XG+hT#~Pn z18kwF0$>iBYzjVjQp9IhXG67sKx7qx#HUQ~^{Y?jGpR*rg^jy-R>j`DDsd9E6~ zSf};^NrrLZFl16dN+BcprX`L^t(jJ4@j?`1uOtA5%hL<(Msdq~EtPVYz7 zpUT~a9bV<3dMTG4L>1cAC7Zif?&-H%T09z-bM_2V-lwhz+e!1W%YTTFh{dDktvgO1 z#ZGRb|C)sgtJb_1E<69KUH+Zhv9+Hut11C&H=9;*Afz_06~$Ei*lVGrHcbrnJb6oe zyMU46Z>t#Jr`<43W>~$hHXhb*xovz;ohI+lK--(+)Z^a3%Cp>iJPi~j2H!Cp9=3dq zs~L>CV^}mBMWkS#M&_$t<@B7~|B?r-6(?ka+PH2IuiUwLzG zSEfyjg5G-iELXwDe33PlMwuIXzM}KM3d7#k%we}&?IcMcw)@k#g4{rpZ7UMX;r_{Q zRjpQ9feNdNjo0TdSWt{Z>Bm2htb(c`LR8t|tIDjEQzCP(mJNi(e?tk9_EYyUKbsq7 zBenkBmP_m>gf@R2fj+>vA>e+7^)OQ9%oX=WFp0yZwdo4;M5u>kmKc@y`g1`lUazHe z|LJy<+ZmFKr-P^g(wk2Y`pl+f&FREYpxIqv11}Q;)r?g9(^kf!C!IyK>R-- zVbtArfJseL_@WlG@f#$ zH+F4(FZOyCPHW{B^)6w|X9tcya!KM~553uu|uFctW!5q4fVVl1HmM!dwv;U!ME(R%_FEeV)iLRKcB9iXJ z9QJE=&yR=kdAj=38sCLjgFu$~n2g$&i;x zdI=s{i(hlCD!7;u4%VSv&+)>I8md2`hgJ@+@*mBeKn2t zxD)oMcjokg_Qiqa=CHpn^=ioh9QK1C7<#CmnR z46!z)v;jnhs4_pBRv5$vjEp~QILwR=2+u#SkkYY*8&|#YCqwC=-HnUu<=?^`^njBmTzuN1G+9_2b<__|3l9nI{1&CJ=IK* z8BLOuALy6jFVFiFdu(8ned-f=6U_M zpk1%;uyT!VC0j-#B7%1JT|3@KV|1uy{}HtFK(?e^d+qSmXW;`zNsL0)g80bbfp;#+ zLdH5#^+oZN3wbX$=O09JU+J^CbrSoqDsB8D+OPOK%#h&w)QNpAc5Z%ezXaH;a>pg}V;k zPmdE~hS^>#Nv7#qY<;bWlL6`%1SpldXko|AuDLUp3lG0A(EVMG(>=mT_3b^QRUL+s zu9q~~+7a7&l9v2S(2g(FJogZ!#J=$@90{h1w z=f(k>Bh#(L1J7{|jlierMMT9u+gOIE*e7=EE-uI0>)kxqMLHsiTh0VG{rSx^$S-T? zKjHO`VSMvzkjVMU9d5R>3}qyPm{LXMFPlpgEbs0Qa{wSZlRNcNk->&sjvqB)qg*mw zTNoyGQb^g>Q+3wBDH3G@Z-u@b9eU`D7>k{nov9HJ(zMur8Qqe(Vm64^*MK~P-u6Sb zY}88O?ms>5ifUVpkWF3IGCH#9;l0Ame9p){z=J=NtK3Km~7&wTRO}Nc~4y zLg>IeN|7pj8JcugXz$ISM!=JXs~JFki&~Bicgu0SvSCp`M%a-I#%2>(|0h6sz?8e- zlKaLsdL^vCSGBNrUE#un`n#}TM{$;uajMFr!aqMY+i@XujDUDR`QNVHfX0055D1!9 zw3rb5i7s;B7G19|qCHl4yaOKyW4Ho9NxgX4wPMr9#mZFBo4E)r5ugnL$YO^8xjU?7 zqIxD^eRqrjoUIs%ibK4z933o@dI5t*aL&1QDkBo2vlaeU07KQm*RK@T7jaaIg|=xjcBEK@IP9;)+ic%g>*h z4_8=9!^g1kV+YVwS&>;hM37>m4wR6pA%Kfk7l1xY;N@8e4`X!b#~dV}Hl6>)n2OE8 z#?XMJ9BhxtX{T`LhFq0=AeylL)Y5oOD=~0Lu0Un9>X6*Yl~MpDt-;L;qkQ<(B2<6? z{q#V!vWr^4L}kGnxGfMxX$aA!7ZZO$17IuJxZj&rHDGe_+ZU{euqG?28Gg~=3Pkj$6G=w+)b zAA>3YkcoJrX^;cwbSm7aB89H4MXwc{I;eqNDS+H#0Tnhr0!UlQIUB>p>UNaw5S&6B z)oLES(EcCFsFx4G{oz_XNgAfDPfG zb-2)a0X9Q`Cc^%>2vji*V0Qts%+;a^ENcajDdxVR+8;g&l zLEmp?V6B$bCmm>> z(P(^?bCDsu_{#)uAgwbye~wPL@OQ-p#JRn=46lYr>}h9?=U_8sO?1u(Q@SZVdB?|GTMEQJ0e@fsOKH8T0KMyM4N+Q; zP=xvWl~@29FNf@@U@{Y;aDk&60J;=X6<{gsHpHDy=YZN==z}~239veBlo~Q|k<}YZ zj;Du#(R7rY0JE!dnXjlOT@!Hraw_dzef^UvZArbc03*lIAj|5BbUqFQ48(vs8zeJp z2h=g~mFO*kOYB7&=2k8+vfPy*1D3FWk#`^s@uM?QGy%#>R&7u3@Tusa4k?~>08|B- zrzc81C6)5I{I+J{%I8&O=2duFn~LE25z%!#Ts@QvsENw#c~H#ttGmu$Fc6A~j=E)) z*R(~~hH|jCHUM4kz%>3P>!|KiVJMWLmdhSGsQ^F3Bb_;)4SS`8~{#d8XXC)KG6$F zv8}{`jp5K?Er1{rdKE+X%3HH-x6o`DU}3^JK-h)8;NeD38oHJ^&^>(2h+biS5O#neQn-A(htwf1E{15r;auvRq8%)+n%2Pi-*BSp$R6_GDAv7}u!y(U)^*4HHS>w@l#d#jeO*r8*UzhY;K&U1G&As zR)vX#GszEU;wEYqwJQnDz^aqzKqjR0&*%{&xKUoR>G5vP;}g49<@f;t{8OCt5%;dq z-TY^O2F7V4Xuw1!>{tV-H*p@sSAYX?=$w|`=Z6e;5CE$1;>cUB`$JbYS5;}8Ng0Dr1q)HMgNPDr#MQzc; z$rD>34G}%SiiYEA+R4DgwANFP&1kR90qd$HWCX^+;5!mF&?quTL}ZMzpu>Lxo6P*XZv}8jhMglMHr_maMf1~K4wDTYM z6eeWDE}@Iw4P2=S;Lj`>7VXc5bw&UJ2TTPnZ+voso&Q{NQ%hc21;Q9rF+dieTl|!%FPP(_4#-I)`spt2 z<4NrM=I+;ruDwSzfOx7a6YD~X{)dA z!pbbMf_K-SonS-)BQbFG2k5VxWn)iYm6j_V4;b;0&0s%r17aZgd$PE(_x2xeJuQIA z4<^^}XJ|5g1`O05D(i}O(xzc{njqU!Z?xbQ0tLRd!ECPs3$WwhJhKW*SMTi~8 za3RK#zE8U*iF%jU&WR}8CtnJ(ty$9{Y-ClUwuriZub4a@cP)_q{XwOm(g#DPqatLR z)i3;3;i4Pz&?7^?RD|fXAE-A^K!h!1Ey@8pZ+{qyguv$KTOYr?)A23t*+-n-o2wL5 zRl$+qyFgPRHiULFe-B82u?9@$xqH7Z?kT}FV+fNA{%pWo_$}_^H|bv*U)BJCd3UNn zRlArL&svg`h5x6UL002kjKUISqViF3J|@VM3(yS+7zk6sikAX%L^VKAcQ(>RsGw*> zUG_yF%_ufRTit9N)gxt7DK+VK>{QYyNEw60<@1LjsR?4I zj@0u~xrH=P$8a%w3wdKug6Z$`-*9 z^xW3l`VNW$MLtHo^FG#k4_6VZ`3+42k8h?+x#$by??;wTTY>0RiUH!lT&IcPz^hK2 zwLSrq0PdyChe4MakbksNx|Z*`=@Hd9SUNhXFa5F_n8*zju@-oiPj%nvNRmGb%8 zYoWl>W^O*^`(IhkM}EF;_$O83^^tVNppM6T7yibv0gNrO|FF1+B66#>Qg<-13<-~t z&PKn-Il5h0ld9Uw##*;`p|I%xtHhi%LKcPIOI5wuS##eV3Ee6A6>G4_7m z0_PmOvSX87&IaWm;wk#_^UL@C&>YC@u3l(dq>jc({paqf2UlHdJG*?aoegiR?e87v zx&2_G%kujjLW!=r`WV?d6s=dpMQf-PD~!%vE_8el*OI<_<%!ISSoq4TN%Dro8F?-2 z4>rMeGqbD8(}5$*bFPSEWjjmg5rG>=w)UiLBE7_%Ok7eA^jnKAD0&5}nut&CG^wNW zRhfeQo;FRTE~h*+Yh7YI+H`K*ppX%x<1TL_;~V&^ zK3-BnG_1$(7i$}2Ud7*HHv=2`SAIO?qRvECxNoV~hxF><-%wbfryI`^lB z3g~`nXA7NVDV90P)+yB0_6x~9>8V~DOUPa1poI5?;!=Y`yDQ0vDa0Bc^RNFnyZ*l^ z4}>9%hDBw7zU2l)qN6dG+jRW-hBSpqaTLY2HPQUR#eFX0>GsW2Qb_GN^KuJVne^+d z%jbtX@7?e-rWQE70kE&@j%}_u^B`>J$tC1rd&2urlf*;kZ~5ukLyVr%d6e2baYnmW zAvn%_v0o#>G_Pfwv%~Gsl56L;T(p~@$JIZ|i}KgPYR%`~n3U-)tC8C`F-vCTshu@` zDoGtQsx6J?8qZaw4x0QTCe*kZY;?P0A3Jz+iBL z26S#EMPLY`e{_4dzC`6w?8IaFYleqEe>C|kx~o^!pT@gu#Gxew7#6WCDN9(T=k@8E z!g`1>wvLN&=<|2=UVfOCN+Oz*cjZMY6iv9R=cSWb?lo$Q!d^lofMwJzi^l$7D(a7t z*Ar_fiL_J-a~;9GH&weQES!eqEd=@NE0w~ja0Q!xK+;W4;*{iErQ$kgF}TH?x0%)p zHrXzvE_hSEyC&?CcZ7W#U=r@Ta)d4iJG)KVUx(|1)>q#axi)6*qxh?}W{gS(tG}swt zDSAs%*vOO&D*Suww2Y5q&OMDhR3h?l!0)q9%0p4p1(*`XxJo)YP=(X(@_P6Ej1B2& z6h8q^nJY>+VyCJ2iFK?VMx;-f_Ts$pSt7Wq9QL81!S9ux7G1Hq~ z&<>bw^)gGohBi!`u}`kfGEeE%H3FvbcV$>@v>tg$-gBMOMLI>dLy{MZp2o59UQ@}f zX_o9Bex2|oP9zM_nPba^CiB4dz$RUP#Tw5~nNzb^V%`lgeq30%DM_aMnOj3-N#)19 z&C_+qxoYpYZiI_PB%?n-&APKn1$FV~C+7Mb1CsVlCc7IICr2ZAt4rS2mOx zWNWSGqVukN09J=sTp}lO(oSm#W@2o@mTKzoQHg<%(%`RQ1-@Hwop2mOvM{%EVV{$sBaJ+#`t#%X+c9 zW;3;0nb|AQu39NqS%S zJI(GvPqTA+PQMkZF+Vf3+%~h)W80zLur$xaeRp?4y#Qfetso8Ky8U?5j2fLA`DwO> z2F*%+E+m(L=);BUq@vVBQL3GvqaeL5>~Ux)*WxOO)BS+LxqpSVtkOdNZ6NY0+z+%K zToafUvMrUos3hpKO|c(OQrkxy7kv1;YLK?}Vbbc&f;R#C*PCHyTdtPeh`SJb>{9Hu z6a9pnaFW()NRj(R?Y4+cQ~IdxahPsLT2+LO_^Q-#L~)8M*cNw zWi>r(S3L4fpL!E-O;5EvXN_J_&))5`yyr=8e!1?fa9J?1*X+6sqT!D`Sa>`n*JAcG znWL>|b*^8IJxy6B`myVm@~>pd&%CNeyruc~LV zyKg;varL3Q2{HZa?fDa5bIbM<3_m33?p(V?Yp-^BA#1;a4f!BDPKHjECddx|!~jlLL@~kQmddt#Ah2?Um9O51G-!I9!W|@TRqx!((2D$0SF2 zo*{DW+!!UVf-?Bt_x%m?wn{;en8!_t*j9KPRu&WfWa6*VDLpRe29|uf$bODV*oKIj zM3iAzu0)DRs9^>TpXPNmwQbE^fGk>?{HuOn8_vUnuIeE`m0Xq!m4P0~L>^7@+g@%;{jQfC5#7ZlUle22+v0INc^Ok)$jt3+fY*+rGkN$r4oND+ zhceSW`RO`VF4+JxTU2ez=+dIXlC&cmLq~vX7V> zFWVCwsryf$xBM%WoZ7IQhVq>AwsJD#!uxK8+qfBWMTW6I2qkR%v~bHxj(h_XpDeZ) ziw{~%BuzX*4GVQ2qH(SQs2nECSd@i8thfhNiCT=ToG2LKL&Y4wP$^_D!Olox1H^>S zEQ&prIFpnru-W6uL^rrna@dGY)XC>6xk;eBlqNqCL>3Cx-~G0&0#<#|NpD9s%i?McM2qYJ^4+WEW4CU1%=(|Rn;#? z@(D-ebt<8(vX`8)%*DNM(s7h2n{?d^oq4u0N6yhiQeJ3s(l=c0Ba0Fy7a05akX|>G z`Nq`#+xBwy>E|-y1rs&%qQcN`$~H2{Is!8v1#M~8vKRZYIIuk%3P1%iL?;;l0{Jkc zSO^g^s{KMC{EsP6o;rrYiZ{(C!Pd=6dc`Os@g}kcdSa#q%SIWolKsqA_(Wp1$-tG; z8aq1Lf{DIVj+*Mr`Waj^QRM6{@3`t0dSfxHBahDaSdH+)71`#?Vz-%gC~uhe@@Y}p zFbdrXs`aae(hag1tmF-&hPdi#T5KpP!pezN_w#;DD12m7Bv5h;N|@Y)>oPK;nVD>> zTU4lr=(WoE+h{8V%B$FCHo64qPKFgs=9$RO#7;w+^2&ZiYjJ^RUyMVJ%vy&13O{kp zR0w1(s3+?2lYS}tHOJ}vg0>wzlU;kpPo$+OU&HP|m&9OJSHRU3sQf<2R$5~sLZa1c z?7P;>n=8C;hZMXM%RaNoVAJ9?$X3wgz5~|GL_{1}nAa-k<_IN)i%(+rELU0PZUNhf zBePO3*3LIQ%V}6#3Zsj(JBluzw~n)*Ui|jzDD1_ObZE3rvmPF7dbzAA?zPIdgG!$A z7qEUJOji==P!ncpGcYOodq`FB$rzk_# znrS{7RvRue4qdjbylnsEg5~wgPQNcRHCtUBS}hNvk%(ZcUn2MGt^QA1SO0ERY(`|C z|6z>2ge&W>U)lKNij5}17@>b7Mjdq}D-BQ%A;sK=Y(>yWl4m)D-2#J0$FInrSJS6Rxu5+lLVxD80?g9;Pv1tPQp zgQG85y%X&#hIX7+Ksp1@`C&$Jt)~bb6-6EApLFmESN_Egul~ahX&qR}zt|ys7-5GB zvs46`B1}cNVGj}>JN%@{QPxpid6~KhBd8BN^9?o=VAlbtM-6SBbd(tvWhhE_aJX@L z!wqBmn=cRDj6OrP6Jj3wX;EqTUF@qbgRZOo4;(oAPWTTFg!lY|0|Cq5e$-sw8=Ozd zWEudMDJoQ5M%X4YQ^%`NaVcmTd}~Sd5~~~(k$L@1Wfv}@+lbq}jHrst4$u4sc1uAV zjHV3VLWnH__6l;5AI{Ah3m}Nrjq4JR+WZ&Tld^Cwm=HET9Khn>oBoaeupFFF(T;=8 z6kr3zfNmaisuE*N!z0v42cRJw4I!PY7cfR#$V=9{IlM%#BmJ zEVkq2h@SNe!V??&{*Cm;c#soW#;6j54g|ttgt&b=0ME^VJ-Lu? z)nGJ2L74!CRsFIE{c8buTzLh{ym8|jii{*a5ATf5!x|$>00|hw1fk|m1gNT=6#MW- zAr2VM1W^&AYWaYi|Bd*pZp!2$Y{y2&aDh_}gT93S!e*>4>v&L0YZRez?NhKV2mIN2 z?++15cY$EDQ6=G$02`C^BK*WJYJd{EzC<~EvB#-@vII2w8LpA|T2F!CG>w>^KqF^UY zFw_WrH0>Oy?slZf4g`Q)K&=(R`C*WvN$`Iu?jieX!w)&twg7--j-f_{vh8^pfDtc@ zmPJn>8Nl-Iec28H008m79eti^hNacz~d4crbxZrHLzfh~eGZ z-l^c64lsm6G^2&>6@a1yG3)U1tv6Cp)OP!R?{Z{h;kX3<{mw@}$ z0;`0uA+0}#2KmR&j8%zW$pNyw4V(W?{i8nA8kU?kGrsxl=v8rp{hVvsoH37{6AWx2 ze^~pHXh+rfJk&;&Dy{$o8cYPtfZ+`zE-51b5mfU-#`qy)H_XMP-l=p@y%q-qFF(R; zQQlJd-ZoymT^Fg>sws z(Zc!1YW-~=hov94P`f>H#a4p%n+g$oLtZ@ptnZ$B=YPQwwFy@xwr2L6(0zXh_j>vB ze`vv{myS=}7}8g4UF4`EqVg{o%bzeh@F6yz`1vI$&qWEAUq4J!{2#EKD7c&YABsN} z#vBMp1*v;h{4emKLCy!e!{PxmMwR?WqL=&wcGV|!VZMJF5X45f9h?-&Bd#?92ZXOJ z(L&p*&zM;qbZe_V^Alcieog8VNFeAvVNAgze&2v0ZM~q_?*z&fjhaO-1%%JY)9-)3 zzjzEdKizLD0F0?~Q~+Tr_=#j7VzutS%dOU^{BqK4A;lL^(D`mx#4Od>D2?&c+y8~ZO>B*w*mMsh#<^Q70d z38QI{ll?r?MN7X;g!KaeI)uXO%3hY)d+3@b@@^cDFV)UUO8WIKUx?tW+lsyJHh}_W z&p8ZGA3;T%jLBw+zp7fv*Gi{SOgu^p2rO_76dF#e!{?=Wtamf8#(gJ_FppY)O0Gcrz&>P zi>ZnOny%%6U&4FxbT&pUBfK*4hH}K2r79=1=9UOKXw6( zFC=9Z$g3Znpk2Iu<#RU>CFBP)(F%Y^5|f;GoNF_Bx3TQo{?;q~aKO zeLd*N-3;7vjMlq3f%gP+>kKSoZfKGB-11m?G4>E>Y5ai(K%LIw%m+#sauWvsjnV{n zYxj}Om6wz-(%b=grn$0U7|{0Als@5%dx^(}EeRMO6lrN)2Vnh5g?8WZpGzMK*i|~f zgcYYKUO@a*Se9r&#Y|S~7jqpVjIZ)@>zzw~`qvPAs<)6&HJf^&T=NyN-K^$T_x%%34 zdVh7G*^#^f3R;C!CIlaDSxwk+pEvqMK8vN3Z!0>tXa3xRPN@~6Et@hkJIdRA693(N09?TAH%!-FH`}8ny9#uUXeV%(onD|2NMgnyiXnIN0_4 zsjROEPi*_#wZeBplMUr+>!HCsIa2^69a}y$vbEvE<_X7@5Bo7*rLBoicWjypeYWkt z{{K>m|1+EY|JR?DY>)*gN96qfKo|5WR$-MDl>|)I9)`S@qL-Ky z%^p67u}exUFB4`G&SMYHWee6hDU@sMZp&Tex7YU~?TktAz@54$S7qpe$mCAHV#_#M zvC0x6myB~kA!yET1tWE&Jmc9g=lLXVyWUjU*1h+6DoJ?Q9pUqi= z*C|Z6?RoaBrnk0+=qHWKGMiX>n9v0%v_@e!VTyYYXCC8r*EAzK(NDD9-0slm)t|yf zc;7oqyr+KjXz%h|XHKO1X_1Z`TpjwjwfXRQJEF@Mz?qRHPn`}=34<*6L1rOR8zTSv zx=H92Waj>j2XQmU3Yfx5*#=I5G0DI>w(aAwKT8YGM{(<^G-$U%(=01;WW89FPIR6@2SnKz>goOIB0Q z6?G_}%7rLRcKlKX)<%1jlBHj(GRJk5I~1`k%tcVL$3=&ePEhBHXew@ZO1Rh}cY1nI z&jQmK-b>txL|Uz?p=9Z@mk_E4@&PA#(NpfL?I>~>=7^(+c~#!H!aQ8fVA-#t#4(^S zgQ9E4G|ts?(?EH|KEUoh7A@|(RNx?@i7uATG}oRoGd7P=lP1YoAP%7yRA?&hGUp$p z|4DuJrU13N!5{MKCCQTxost35h<;20wI<$8X~@w}GXl#p@*}*aa6^w;=Vv&9$V4s* ziP+KuAU^u{u4#dSxX5x;?ciF(IBR>l;xEc+Vr&eVy8Y~gCM>|dfu_y3rT zzWUIw@#~}r%5)U^xdNa8trEnk_yf`zFi`*~Bh@)wrz2I*LI8GgH8DS$N(NrIp0XPFXQ=xcwe)GI)toczn&usZx`t>M9ncF6UrR(I7VmbSk@{s4M zf-#UZhf-raIMaz!!DGn=7zNrK^POnr6T|}Z9B|!;CBSV88n%{;42}}^sbf|!L&Fm@#S&U7_?BBn-G~mk~?5gb+4>OrqXfN z6jE>88z;ijBU?mRmtOX|++l0fFM(Pu8F)6FV<1FC0Q%(^7NDH9l z@Cs+WgWMk~$e>s;#X%^| z5N1GzUtrv5)&|>SUDu4|`-GIbuY1f7*}_%0=Sa@Bx+g%459A{oU3nh@cye9=eSu?K zSYylT&AV=id{JfaW{q0~V?mLtL$MIirx8`_qCYrp%vHfB95nr^PqMcfM8X1yDxH1t z_p5TB^NY^~6Su0ZL1Ax5WkTWRsR2T`26N}wCn%P*8#MQ@N|l={(}|~Pnyw$nPhPy_ zQx`-U$Y$dr-pN3_03wlF41z`~BCJ zk_9DGHO^&`-8rXC zm5rQoKunEiZ9!y=%{H3}?VmGN4MlVC~fP+7H_sEgsIVvPZUTllEf^(;Ag z5}o8PVV4&&;I&;>w^STSlu~jrI>~dY*ETt!$f&7>LmAs z0^^89z_2bTLoI7gYZFDzShR29o;^`JO4GC%uwTMs zuIDlfmj_V}KAi<&5|#?7Z{W>B?jsZvWt>F;4fUf-Px>nX7(b2^o}TFmK5zlx zA*&%aA3GKcFg7Qp&Fi_34VM&Oxv-sk-s;XVqquGmfECH^_Y~wG6`S~yjeh}0K}Sb@ z1{3`EavCSkWwNVk5X$#E?n^6%mG_%z_sy1B`MOjxA zS^6&Bn)dqJ&=C&L=%zzK>)gurawAM9ao1|yw`WT-(3WZ6cnS~2^n(rnLmss~uhoZh z?)PYm^gAEDO2M&HS%2%x@KrIXACHavd3nkEXVJgb|FV3SUu*qa>ho>;dTwMn^poh9 zus|w*-uLhO-4}n4jYz-WmyZ1X-0S#9bgu1Zudnp$F{@>9R@<-VBhnvN*Zf^L*1r7C z_vE}_&W261&?7qL3jIevG_cnpzlBg#xYQ z&~&4T_5$o-=594b1e1ofU@BYKz|MT8a4*MwDIL|w@$Nfe5}&c&eZ@Ls$QjVE=b@QY zcm}B&O3K`mM6_VUc~R1L7(*6JTE>>laCc&CE#d$sYXHpfFjlmqqMPFJS%86}UWRg2 zx-tcl9+e>XWv8RUpk_YiI9gGeiM#2SzOD+|%ExER`ktz(X97<-^3pJ@Q|J3mHSUF* z`S?Kz){A=h^tn@)B(RlG!&Zo`7eTKf(%qz-=02hu1HA)C(W-(dywm;h_>v^8mh2pe zp4*e1YgMa#u{2jfQ3mwKHS=*MG&s|S_^3ATMu#jcgEn%H<@OONMBJbNYbS*481O~j z>0%OsMuaI$TzwP1g^2Q{9wdzB5j&u=X!Mn609ep?$j5Xx5zmn1ZrLbX^JE_nxhA50c0Y;){D>VTT(f0h1|-Ms~5l*uAm3~aQ%Hyy%3wLP716$ z+nGgN(~LJ+C|K74Sx}K{CD<3@)60ctcQ0t{Ei2joOPlbEOQfG|ksI&jXvd z_+%<<#mCeOAa*oStBlAzj=n8`y#Q#32zN|?JS4|>REjGX7OV?{>bY1uYS!gnDawose~nM0t1h%+VTuG> zAu6lq<9(>wO3j#ylekbm+}~UtBrG41B@&~yGQd0F7LREX;mWwB70ilyTE%8E#Npy9 zu;&j8HFCJPLn1_(5C?md>^FwWcyb$g>NZqxD-n@W7x;HFpPc}qW%HpRB2B|@qj2#mgq{p#BY~O6F(zb$54n`7oHo~U zroInf&MTyl^MBdM9WaLM>Q9$5PFcj`zb0YUn?UU%TwN1>oR6dOp=V8(28EZLMVaBJ zakp%>D-$r%K^g{mJ}s>NbUnHJfN`bM&LVqmHKiF}$N=Uti|dzgTLl^;&x;Nx_|y! z%ZqF6nhh2JrioGW_E(A(89@=%)HP$BsbC9FPFYmb#K(9sPv?*kCX!P}v7C#7(tOSCV=mXXgQD9~eOUW)_{DaGDq7Bt0UHdEn-_qBtP`ijOY zal0;IKh*R0!5=2nC{(#YZl<3hAKuSbjUhTR3z|i^alf;X?`!pU-q?zfwN9irt$Bzc zlcK`{C^#BW{f+gN=vpu_gH*J280Q%knOA=`MlGtow0)*m~ng{rZ7qu|%jJ=>y zZ=rE@!fo3RdEW-EZl&?vCmU@2^3mqVcINH5OkAU^nmBq_@7LXA;=NN3@bxbW3{9Xg zBF@LQvW|*2p=SL&1~FHm>qXqm7=T8u%n>1u32{|qC_e`F;$>SrIG>75dr*f~)x2!M zfN%9{C=0Po!~4sehPO+cr}zEs5~V&vkq*-wRW~t?o1J28z96WOqX;4EseMR9_=Vk zw&8(2Q>|-7NM9nNkoGVrzD=g$iohbI_qEzxVu8Q=+nV`U3$pTxdc<*5#6uCLi-LHU z+ z&hgL&{yDIDcMUHHfX(|Agv!JgtzmlI5zEAxUsLf=i!=6D>AQ5fZX;zlM?r&p$|5G zUwunTsw;Gu)B~Q2k~8WaZnDuXfSjg zjU1l-fO&N#??qRh@k|_0wCaM~r(qLerwVyg7F%9JMyb%Wtd_9HmKg#e45xd_Qsj*Sxe@watw; z?7|Ziz6TeYan7Y9Bi6z!3H&P-KKZ*Z=`qqFetN&dHH-LhH$v*^$6~j*x=_aRt>mdg zK!YARKPwelgMDA^^Zv@?I>-3V={~36DXeC^W?g@Q4-b(#4AVr2;WGT8)3?p(wH`j0 zx3N8CM7X%}3WX+q$Pl|ppcv_EbNOLM+Q)vzM{uwk1tbRU*Ke=DHjzf$K95~Af#@PN zYhXV7ajlnJ{i&;v#e43Wpq6b}pI+#Gev|ghC;q?vm7m_m0`kA>76`C|Wd1g(?Osab z;d=DVsgD_HV#V@zx&!}h{Q1c`0V2hrH^-x!`S5n$TQm8OAupv6{^n`<_5n3H*hTct zEB(h`- zENG!3Po9K2Ho!Mm)P1Q%Z>6Gdi!e!*zp2FXnh#?V>D1GE|NeArObz~n)e~O5u=)ap zJpBT5>FLS8XRrUAHd_+Dn9x`%TwaE!Y=&d@W#x0_POd>Toi5QygAg9e$+D#`_iUK} za`zSh6zA@8-@&8ag^E4r%2#bIw<-HNhbmZ=Y!Xt~(ZvLH%Pce&8f9%6?8T#XqN>JA zCOUK@!1D*(eJC-p1(?a9MbfM;<;q!0bdgNL=0+*ZgJ|vCGhNo| zCE>46hkS0D&FoCG_{?4TKIbh~OQehy1j{0W&LFG8T3aahge&r&|M`q|0^vls!7ab; zSrjo>oU!p=CgTg1lv2t)rGpn!@IF@S$Q?RSXM}|-axJ`&Z=AjeQBN6w)7gM*8vo%n z@p)Q$gxW8bgBEdX3>vUiWZl@QRw$(mta=64U#WVz`$?hIk*4qxpGt06wiY9BowCs( z!EGNpcD>4$Z7Pw)Zq-M{>n`iau5X%RDJHyTA`LU{n-|Zdqc$DdBA?Y90aOXa+rwpz zold+>GI!W%>W>y1izt2t7C&2Jln7w#x$9a-J_@d;7E z!>u5wsWjVF{g1w0cFNDYLk5c3!b56$X;k?~a@Y?yLbfHjf4cO{YnRoeZ`?%VrqC?% zhtre*zgfH0MpsrFLb-m!89VKyLv#bfvN_8ARV9A2*jrclX9g|?;1%qNH0~SuwE;^M zuay<1xkUtp1w&=Ch(LpLKGS1wawwhyuJVd@;JH)ZMQ`Z&L6t<-d}Sv$to?L+N2Hvp zkxhbRRY{?K7WqK@LH$c@+ujoIcXC_Sof@|{Df4-07a{lEqU@30#oY#MgCNJ*AoQ)_ zdt&0zWH^Y!Cgx{%~Ft)qK)P?29{hf05>rNyIFa<7}0p zoW8e~idhef;=`iz_wD$plcauE)f4?w>9^d8A5K7xy1Mp-*XX&!Uml(bnonY<{+vCx zV#)Sn?}}>8qSIcMb7)i`yjI#IBISCCcHjErVnKE;Tt&_}L5m-_CcbqpZ(>c#qP_~b z+S#+*LabVAjZjIIA7R%i@%A z)BX)UqVO}*HH}L+`Ol72 z^0Oo2!{r2jyvF0DO0W)x3~%leJu8pU8}VABTF2;AFyf*AXzwNNhb}0$em`qz*oWF| z-l^p8q~>EvQN7)V{8aMDoH+I<#6>`VgQw~e<0;C`?wRxDV~r|}XqeKJZgQv)7c>-x zu@VZDp4jGi*D^5&&Uc!YPMli7<(z-WpC>Mogsk^HL|wP*JyC31y{MLy`QyRXeJoiAb4 zhc~#Z%8e82g(X%hcdS7|Xs`&k(aq{^Dsq+Gr_)ajse^a5juX^{wp&$~&)nW>NlHrG z?!`Z1*+^-u4?Gt?5S~oFt?~)PGZ>o79s4gMKDpBW=>_EhDBCsPXTe{;%yq5@RIja2 z+PilH=Y`eJX4ObjH=PR?WL#~_<|VdjEc49g2fWjK#Ej}(Q93uxDC$xpYt{ASvEt7= zN@pj@{POV{c!mrs8>y-~hWKP6v7Gm1nsGP6?j(AMtb3al5P$Y^+d{zy z4PQ*jM_3VGax?l$8HNeK4(ti6#~$tKbFI-NF?l$hz}{j+MmXg(Z9MnG&m0*ncA|Ti zMn^SbGPyVH6z)0s$tP^p4J!8Tn&>xw+!z_rI&0EJJ~;g%yQnO7jm1f{ePROLOtSZV ztw!YY`m~SfVSfA9pZV46D-kxln|?1hV>^1Q2fPt^=Ixr@tE0EOeN6{1oi>!Jzc`H| ze$93bk6gE5!>~y0m^;J!?LRNxs#TMg78(7kQWXcr^n{I}GG}*NZL*pDgrxevq42fn zgl8z=9<|_3VP5(>GwWL8O_?X49bww{?Uv}Sbos%Yf?|iM{w*1fJzol5(K>ZbQsooZ zew+U|J*U^9GNN(6u3*uoyZnIT0)&$Za3{lWwTIul5P6#vA>2kg&*erOP%-b{q(5oQZ=_&I{l_^YK(!W@R9`?WXDTiVR029wi>*QjwZMoDQu)R$=_(p`I<&fJ{A(@O zx;k_@G@V#&-0kWP8ydwxeT7S>LGH$d*VcLgPKFUxD~mf<)pVNVO6f({+R)vduWom) z{YbCS-)L4$zi`;e+_GyWteAJ^>e?V@-id;ryXzHlouP8Rd~8=vOV_&YF8lE=$HlIV zaJR(0tM<6F*-#gKnywqs?qn*ko1-}fIP2iM-HW?D?sRYd*wsUKvGpSU{NUom>4uhF zd|RDe@UGvdT>cz(@$_;Hi0IzA|IzO$+6H@9uRD)+FVekJTz#}%cW}Ccmpej&+MQEe z89^?4<{oWby!FS~&8ERMD(Dd|*EOi7XJ1f<_2R9_6n^Yn&w<6BgAFczaBqUC%OT6& z!vR+umfi9{xLMEjxWx9xl~WUIdZX|3oao*JkGme`JW5{Nlqe18J(;p8Nu`esx5U7T z=>~m?bDI(@`?B!8DaC!K%G1$?5YTU%*b^J8#D@Fz&0P=8*&K`AR8-y@W$HoBb*|KQIbP&Jhx^Lm z-U^kTbHxL7@IX<7XR@V7)sXx4@t!R~y$vp&<*l9`oQ-a#o2wUl8yh^U;LVM516P9v zcSd;B*9@LB?Yqy}T-)H$y4)RNKe)cTf9KFZQ(|}fo&Lt4feRmPy6}TH%loSq2a?7& z=QRutH1xEj47E0R+z%K^j2*l@J#;p}v&TU8rsuVxA?fvHueduy&xU&Qy}Wzz-Xktv zF9P}|wTEpDhP%qW4h9WBoZFmh;PotV_;rn2U!vEekKQxg!?j-Bc@5srVjq{{J?1}d zez({^GcCA-_nFo9d13ij((1)K>}s#ld#zzZw93O^gTd@Y_bXn$^x~1lJ07kn8?A>P zdcsb5`@6qS+x!{wD24r`(_Mc!u1?*){}xBU$W4AKBj^kv=ByBVStxf`hwQodNBd;PApaNN=yb zMHHylvaYz>K<=Q24a!q!7oAe{J)2gs2lAMZ!p0NXB&2`$?T3~T3Y4?!SH`yLU2oT} z1zkuS!^^VpnX`p&26uxiXz&K6ylSdn$D*L`j*o{I=-h`sF_Cp#hHWMkD2R}0MAYj* zx^r|^N?+CqG4y0J=+%UdVjy1IfH6y1+o@Z@xF=?OXsv-jyAKR;&)RDYX-PmY40^v9 zabR-X=ennL%D8()fTR!fd3x>l36?tp<&tf6Is)_}W!_Gr`59(}(m00PCla{WS+>y= zUlE!xvMxI77%yF2kbaz+wVKK9`L=OGa6SFbv#8q4WXY4YjC*oT87Gr6y|z7y`Uu)b zXGSx#(){4sUqFNiow$T3k-h;oMzVHSj1ynR zX(<7_j3LaBoifee5txX$>-9i{APAWoL>wZuD1MH$hJ;RW=g6#tC$iCYO!j}*+|?aC z-Ev1&{GcO#Xh$)7_Y4LpM9R*7VgHVoi%q0B_5n$j3s4|OWA7;)GZcWj;vFX$8C2~a z9Y3b$QdXS1J;rNdOCF@v2O2Pu+l`?E*y)ZnLF$)xw`3q%E3RtSauO#pZ6>m*52ovn%*;%W_2sJ)3zBcO+F~msbLM3rl1X{YNqQ(gdT2%I@PcQ$A z%aU9U`S2)&9LQG2pwlKevO(T7K8oTEngY9B#q2Ck(5e=79FrCA_k5)I*$G~{3Xp!h z6un`ICG+|g#=ZKP_v+X6(EXxp^a)UA+G+CUeD8bmKl^qQ+Og?{&cyER3@~B>EfXBe zT)F;KbW&1QsD!=MlS}l;8rwF0oWyx`{blk}=96t#)Q#Ex{E)*oC^KN2RkRd!f{_(Z z3vxM zr9kcb52Gj>V4n_a;Bqr+w1FtW?H(ceTo;l!TG!wb=P+p5w}qs-$*i$ z$4DGHDZ|W|lgLGm5ET6W&d2M%J5++SnTQ&j$qExAbbNDKuKp zO@bnTtdyjz7#?arFMVwvsB1G8&qaGOA>SEr=ILZ}BWt1}xbf2D35`gXB{rSLnR4Cg zhDw*Md45fknnZ*Z2HKDfN0Z>Z>|lR>x>ShDSNCIw0&yEiu$|1lje)~U;SD5Y6dy7s za~M%<_3em25~o1bNU#>NVj}(ch}$?gu#`h5%Y@d5nf0jEeW2{t@COij(Q&dBX3DTr znU9=p+qw6E#{-h?18VX5hWP~JV8!{YBq73{i#}@e%sn2R?8gySa6+lUUZkw;;tz>d zhyYU7@3#LE0HjMT`ll{pDRV-tY43I!=}Lm`Bp^x;-Fln3sI2z{ zDl#o8Q-yKxX9xn`0b>ItsFRXUIoVOEH9F6c2p$|%B zVEt0oiD&R~J||k76^9A-E`1Z#bjh`oa_ab}uD!j_n&6b>;Pq&bO z!#Y`qvmvDW^E*$+2ubN_fmud=vg;q%So$*~YqxiNk~cGnny0DF{POt$h+P?@Qj(c6 zf&LYIgJ8@_p2$k=Lo5Esa-;2As^!WNr{kyw72nbmZO}GUrcrHzr9}a>M7X z>y43VWvapI$L#)W{nCH0?0SBKfhwD5Qcbg z8znFHh-0B8ro9cyhIEw5g`+26tJ+2MlbJp=Qt%&mGb6>rC^MXxX~6&k(HSZ>>3euk zd-zOu=yL6sPkFk>PS3H3ONfq96*tp^*?FLSQ&!=*W6Ae_9uT9wlhPBGkVkpwL^3L& z_QR{&EE5tsjCyiWJA#DtvpW*nA`>w8k2hvqEk9Oj+WR z<3jJHzFi8O6z#oe>&Me`MQqE=>a4}82K>IC=T6jiG|5wzHdUQ^aQs5U688a=bun>q zm$?iMl5U(EQabIq*VM=995r=ln*P*e`)&XIFNd0+pyhSXj>UDgq|fK*#ZDAA9@;}0 zEHh=;K5TVHlLCtyCisf|CFfSG&z+!rzxM2`VQkWt>Tj-t=T>|9M_EZ;kDI$NK59de zT}-9GJx6N2s^&0^B~dap_BBzm@W06E z>TZ2JD8Rpt)1~%CbfyorTcTW`l(cwwgzTqM9j+%`TlMX+@%PpCE`6+ z<}Z~M46R;Iuq~y4bCHK}Do|o~p_l?I9L#-FV5B$kl4XdIQI38ocWI==U&voA(<>Ic#Z+H z*`FAK(04&!(iI(RY72~3s8|%(C2|eVZeR7|$!SIV_+6_u{+<|h4Ib$Z>yl@&JvfT= zMdJI~%gMh<)i$dJT;%O11ZQb(Bs#2MX8Z-WF4U9-MU`Fd6*;XGD0ymJDDuY z1hbSdoN;jJYFrFzky&)nG;ThC;5x1(CzbCz^^9?N#PyuSysEDoVboQefG}yqU&>e` zQXKuybK&XOBja{5BI7XO9g%a#24;~+?V_-p68tQ1vvu+4EDv0n{aWR*&hO=QLORaD za#ZEC0K1YxO_)Em>PFy#y_iXHsrg#Gr(i>mV77L>C$p$D z`~5uy=8>_C0mtPa<@{~-up)$eT60*zPAnW{Tl|?m>}?&P7oq2DJ5Cfk{^9$($(hb z?0)u}&kAixxQ)#x$vT;i784~+|5AA2+wfQUZF_tjra5mOuZt%KJeYlpUk^*ZVSMeI z*t3EMA6QQcy9gnloqcWR@wZ&$O2@Y;XV5$Q1&|w4YK}p;vPqX{934!RU1kkWrF{vh!(#v|+_`MEZnAT%2(nscXIdL; zlDdq0%!|yU?Ms=u^0OSRB%>e{O)>wqfsN4}K8@+YnfjBB^4p251d}hgch402 zwda_HADNqUe=f1qmi4rM)%Myk&F}q-yn@&*MA?6yB{$sh{j+Zw=aWa*3OvxKD@(u^HQU zUGh>s)<2K2E4_4j(a2_V;+3alu&e@BN4w1S9gj%Q)S@}rc2}LUa4}p52C%6iv+2T5 zWaK7{hB+e&xgFIh`*wQF^hS}!tz^_I+dArRKmIqH-u2X(ZUTc_J}PR6Fip8c@FHRk zJnfXqhmf4)B=L9~K7bR7p~-+E-Cce+Fpb0>^)xPf-8frgsMa-4Q*XsCd>GbX{VLCG za>)7E_bOKdJC$4ldz#1Y-Z;AEyqpscp#g*?M1^DJ{Uxk{8UbvPgiv(_Zp3@j zf?Qjk_X^xa|H+*1`>m7@%NafJO?@x0x%8jb zlO3a5e`qk1Ll#4k#XcQPzna*M-#qpHKKFjIedJ#kKK4&+%zTQcQJj<`9J&SNc@K>b zS}v4%CK8{bXYo(p{W_A7E5in5zg8u@`05$B3KO0?Z^LM_x_8^#EpA`Sprf(t2GJ_SF>aImV@)qc%P39G~mwGGNKmB+~xrS7HP>7v$5_zZdbTIHwV1 z+h@LKm5Xx8lh%2ydM2fsh@=@XURk{&I;Dl|u)sYOyKS@^d!+*!2zjsv8N^RlP?XDO zF=UcKN*K99c~*eht*-y{x?|tkOI8RY$?>r#H-6l`{qPg*o0OGC<+7BD1(?uYqP!Y6 zlgi{V))8GuUd;F2sU7fY^sbjbUmi1Sm-(ppfa3JpU;SI#z7~&k;M9bRct1L@B^A^f zh@Y>PtO*o(q4-G5<5QcNwQmW`-!Y|NVS& z`B%4iGkLT-(pe8X_NK;AyF{NQ77aFq)fW&Bo4bv=>s9Zr;8V}bD1b|rjJM${( zz1&da(^Zx}`s?+A^^HQ+s277ALX`|ylp4B05{+O?*CYdg0duV`kP=e~r77-PuO~-j z{wovA5Io=j*^kDAgutsUdRUnD^D+qL?y;@hW9Rnt3Qu<4007heV=)hFzxn4Xz-fa7 zzfi|&CgdOf&OskhjB@_1_t=z1QK31aXlSb7+d~SL*MVhrXhkvBKQ{1wX3&FF!xamr zkI=@iyJ)FZLu3FML?M~JB8UYD7tmM}*rW^8UuG`tT=C~`KlTn?tp-H7cVLoeTitrh zuh6zA3APwT>Q-jGS<*`dY50&TCDL0jRwP!)6pu1hB09hTDWDX=f*NW7OCQ2g_)Q0z zO5!E)SwL=ROe-89+yStyRo;$95pMI%sleArn!P4{#4k!|F%(fsgPD;^GA@hYI43V`DnYSrn+9GKi`As~nZ7B3=QlxV(T9dSpe z5ik>7I)l~T1MonsHq(o*#CTc8=nU_DdkK5T5Fr#G13;+eN<_)kALC_sMHmkShtF1pD*wYiUt zGn<0Y?%>S#iwO>)_D6$y7tkg}0!-f(oF>z3%_~eGpd2x%Xh26B#LWH*ayrU{Yy^ON zM{M;xNUBwArE%qBaC#90hU|C?+W9nu{iTF@m}>;8+Axu1*0ZV-4I>bWt{4&tkS;9b z9bj#Jq$XEDxkX2Gp9!z714|6^)|yKVYNB5wEKq-d9+tqXF;!y6qA-K_q$%u)*+7|XG7(hkq)F%TdJCml&Ysc3LlA~zo z#Ykhdod{U)&yW@~w8Ht;hm2?Eu+*A=m*LeFZ*0ddji`DdT$ukV_MCdW0^>rH$FP8g z74m}p&PCC_U+Wba091bIUrcbTHf`iOhd_;ev}+0teEdBc2>Y2j)USRd1L-U>2><{@ z>kJOi98O;wdK^pI=eqrI1<)!!?p{%Gqyp4_OK{iG_(2;H16mq!C&wm+A_~*Tb@QH< zjk;a(3|uK&vtlN+QdjNbWBV?OXz{u)Dvp0@GdQ0fy=J&_&5O2+SWA#F6r8d4+FYEc zn!Qvobou(s%QY{5pwKE#Uym@q>*$U(DmhmNzf`+1%l?2Vr=6Yu7;tN5s8nZG_r{#h zjpL}eQ*IUGH-cyU*S$Qqg$)LRw;}XUYWs_}UNmZhT-XxUT;pwJ&}X3PFJ5Gqa^g&n7ZOBJ+P zYi!2W=XM1fcl@Yv;DNE84k<@#lQmN*URFp(-Ni|U^)GGB>X} z|M~s?{r&m-Uf=7QUAr!?>vFwbujk92&*$TDzukImen9AlVEM#uC2sHLS(GvX=GMou z{H^ihVPENOqK1V%c6E?Veb~-_yoMpk z8@e@sBZ?z6@s#=g?$wEHxQl(iclPMA+w|XLFAsfzAGic5KzLGMB|(&5H|?8e*_4?# zzpweuytOxR-TiXMF7GT~)QfI{mJfE+*E2?FBxCAng(3t!p4eW~vYv5Lpj$fCv~2fn zH-~lCcTWuMni-aP4fd|i^+7P$ZVEyV*z0M3-`b5so`g9itY44lkvq@BJSfyJ^ncd5 zwmbe=6V|6!xMMu?DcAX8-+R@P0xw6`eeJGAjwF6N>4>9Sb-UXcNB5?7_v)POLEqi) zI+8g$>mc2@zIM+y?L5kJ1DkIlVhRE0Kk~MnqUJ;${Ku=$uQz=jOwgJCzl$tLNT1~Cd*)oLg&jqSR=!S47eon z_a?4}hB|HC;m#p3X$Dw_HA8!J8v2kk-JV@NrfMkMc;D9V?df0kE)z_Yhn z0+^HNnF{FV0``~>u+?xZ%LbTZXpdseA|3*YrFp zf^^{S{EKL#?6;CNDzP1F>U{(HIEHF1jI?j2AuNsfnt8<(%ui>YG!oNX+4!iMsPb@m zdxh~pty3ma|K_nY|1)9HZ%1D|wS3kBJHSoZpm9;bs=M=U`ux$1r>vT17h>W6Im62TIYaR|Q8yc=i6IZ*5_@e=61Y*=PEeouV}mg@-Imyn8~rMfr^aTzO@u9ANHysdR)#U_=kmJAoci|f!5 z0lV}n;uUv&l&~{9p0Kr8p}jWx1xLUDo~le_eEsxiwsJL+^{t& zIw2|tjE<(okc?sj4#aMZkM~+nrw})UQa2`T-=br-#dO0KXL92CyGe|#Nd-rfj);;? z?UPHRwwbwXtL{wO9G#ZAGkq_({Xo{v8&7kyckD@y-0R`JHz;mze&Ie=d45Rofy1W@ z7>5g(#RdF|gZgfThp!eD6@k@_tk`r`-8EMIZdTK6PU249twFwj6raZ~8ToW5E4&Q9 zp{%T=>}XZ_?ycqfvkq639<4cdJoU(ly+ze|)xwZ?VS4__v+bw4M^9go)Lp!K=J=8N z^8EVxll2Wv=QLB!#b=!R_`7lcwey{Kno_cx?mliFdEb(HuqB84kGSKCsN+g|$F<9i zH%=YAaijCb!01g(*3ApGw{~*6qQS0n9a-|2j~ zB7gk&!N}CY=-hu}5mjTIC&xygJ%2Izd`bS|!Oe-D=81{%iC1qXSC(JDnt0PZAeFwJ ze%3wn_RTx!-ghq^zF(2g{`vcH?$g5DGuf*F*~;IqvW3Nhp2ZdU&!6A_`}=q4``e{o zKYz81|6cj~=j;1FKj)TzyjfoU^>^{@-{n7l<$wOJ{QbMKBLDJCE?<_fEUjE$T$zxs z%*t23jjnwEwetJj%I_a5%b!;ME-4OLkt^O6hcB+I{8?F9URhCmUO~$NZ?s;JSk%oy ztGEnkHv0;=I1^PrQFAZ41n-zJIq+1(vuCA~=LKyL#k$YYmgbNGD*j6*KdcRIKRA9f)O%Pf8w2J1Pho%{|glL$yX1v0T^|BGpod%;oTx<7n z)!LTLkFF^+yUgi_r`KPP`1AGL!_8w$#^uGmXM?9HxdrjT$Ch=p z*ecAjqs7YEW%kGuxO^FdiwjSmj5z+0DOq1>MDsBzT4r)#h?s!44OMX&9~*`5V{mHz zf(ms^fN7FqeV#vz(y@zCW&Bp!Mt=KE3_~S9Y zeOUfvWQjUoDuxLo!;LUH=V$X$3|odB1gM_Z_S87&b}-m1X+Siue|;hZs=Iy=hgRPj z>sq?jM z%OEIBvS|Fn84#nZg)A+r?>~z$jrXxEub9F>)p9MX0cc9g_s8V_=5s=kUx&B&I!^lZ zo2}=XjS({QL@ib+e)cf)l!~M1TJNe~EOd0MJrz;y$9kZ#&-4nM9ocadeI?WS=v7UA z>r+#FOLNjsig6zkE!XjzT+g+S+Q>e zDt)BX{9J7|1iGVDJ;#YkqJRw!i6BH-Uz_R4cT{Hr%F<*e@WV;JT`=U`?oWZtU;iTi zBfD?6vb^~3F2ytZ;hz=x@_%1eR)E|$iz8`T{b&^vBMJ3d`V_a+dWXt|9>@zTdrpjB zqSAWwGfJq)`SOC5`qhm+Yd|fildv0GO?j>nPGFl%PHD8R5p>|L!IQZyY0r(^#MR_A*S`}A+>(!6X`&_=hHh#|pVOml^v%*((x0ItX;Hz!F zF2L3Sx3&h&P#+cXRBJepR9dnYzfuX(B2&&-ev2Q>1AZm#1|5SS2OZQUdT=lV`a9*| zT0x_ATJWQd6W>mB4mR5Ed+=z>?{C!-?0F(Tcra*mKopkY=6~a~*6CoR0bBm)NJ`CPgf%4p-5isIWv@vd`CRHvbe1_Lw zcfn1;FAzCa6VABRV4@1glJ)+f6U<$vg4jp;KptYE)(k#GchEU4Wq_#)_N}Vn&<9O6qm2yj#uPo1(G1h8RCYOZ zzFw|$;Q2X2FG#;rg0Aj?!yV`C9M7xGwyc()d{kiScw~d3ldkn#htV^ zt;21IRN|i1og~0EylqO`g93~a`(Q1JXl*R0OA}IvGa)lpv}vUXQ9pcdfx*`psHWNO_)0w zDQ9;)!&eS-?G^a(WP#);R?iYX?WY+#c*_pX%NRrk>}T6S1(Sc?#5`8 zLK5g24~mDb4fDen>y;0_bo+Po#`^Zf^VMyTrN?Xy#rJ}lX*b+M3gIRIMy^0=zP|ZDC|JLN5CjAk25_!4}gtx1Rdl6|Q>ME;E#bay! z{tzFzef;R_{8OaAKRcfs^d;krE?sonml)p>mMbcOBuLb1h}>_la4ah)f|fQffCR1J zKrAjgrLqf^HjGu@UyJzkMxWqteTCyjN3uoPB44Z z^nNV0AQqoPJK*{?-<1{|C5GB5nomK@`6l{>ctnnPza4IeE0I-v6{;d&xr*}9mIn+J z1d+ULP=O|*4dy9@-mGR@(F4q8paBec7y}+M3}?vL>X9)hUZ4Wo@v<855+QjN`=f?| zC;$NgV{b6wy3a!#6VXjf308$x4?^})asiv!ULSscR1xiGnBuFEouA z48%?*a;FHXP|>;*W7jAqKtwoRoaDxGbmKU}2xV{(eOIIerFiu$X$;M3^jHEl6cxzo z^5LK|xUB3rUK4JK=mF8^|BK}jb{s0+pTk5pTL=Wr`K3gGy9`>2+tIQQS+XRk%sa9~ z%B|`ww@t(ugYZk!B~Z7DSr&974PG+~ojVVoGfxH83z9zs!D$7m0AeSxR3U%m)dT#r zy%v1*a|muoOv+OGXjl_bE>#7N*A)FE|ZCMa^Vs%YN8Oc7_!w6WXt7(>@`SiKt zk6&j1cM*Bw9L@an9)ZZW=A?lXs!01=N}+CM(11g1&utCnV1rt4U4xtE_SpP~iLk>W z<(oKQH&gjy1;R+-9+d`$qBWsRq(xlWBY`7IjE<6oKnVywU1zg@*{+bX9XFefX9MTN z^?Ua>4b#iy_k>N{jm@`YK!08{f_Zq~p`0UD#%`a^Rc*WwjchO>HI$vba2gM-Z$x+y zkzpdZF=5XJ0<`-mLKX+#>WWL;7br;vlyQo$am;l<`H~3jfdgzL2oazx`HkKvTNR5b zS7hnT-7&%r^eqsQBT~A{tQEzC+!ZI4%9MQqRCbEdP!IzXwHDq~e*U2ePsBu3I5gAh z?@BJ+B>=Z*z&X$XCTX2(YPINZy~_l0#LDIq&CRq+M;4WPb1tn5`NssKe=Oqy=9aj+ z_yROfH#-%#sYE@5h`bw!(-UI~MTcrX4>Ax$_#4io~) z7jfvVVnFu_fWrZv0K{nx;0&Nni{YACI7fu+Bp@`!=u#0Vi-rU}-q=q~3>n8fb2cQ&ebrUORFp<_WWlbWA z++UL;RdAxLIvc@uewh+x>PG5dbDanhNB;7lF-jh`4p}owRxZv)WChRM3Im38ED|=%xzC-4|hMGG$tYQk2-C zeE->V#18F!01Z@*T#uexV~h}=E7{booxmD!Iy1T!x@H&_p3iAM3t=01ePy?2MpV% zSHP!vw+?7SRWV2lVw=YLF6Xa`!3NR!*cCnoD9P$DirOZz|IsN(cq0&E4qR{n1`Y*B z$(nM2rhDHzVhQmJy8j%=x_XNM$nQ$8qNK=9*`XpQ42_v298GTq3sE3sOco2dj#ic3 zUn<2!(Xwxep{(W0=OiI%pz>N!IaVsUPrLk&;I<;lN{T;)`2Fx`w)j~v&K!jMi_|wN zpg4{=T`?qChR$U|g8f{UcipS0hsaj_&;u0yQw$U0h68XSV5tD2 zA=0B216UEzzM%}_3gDU5^EuUO6{R^X^(e6eUWA;BmpGdso;&VGF+&i*)46EGa~Ptj zs&w^1d5GabfcNcb_c`R-oN7oDEQ4`SwK(9eU9=k=Z>xjumbL7|Ot8O8CM z6)p5y-zV}7zcE?)%wWk*XZM!GV3{5m` zzP#-2wXk8LJa?jgvrbdk=$*eW*9ScEpgl6gs?(3^#{-ySAkvXBD8fOv6P5Di#2cO! zN{1vsp$vVD(R-4p^u7ynY#8$5EBu%kb?nx2bt&o?QE96bGNY&MQK2LfIpr_x=y(LD zf^UrGU`v1Dp;8!u2=|i0B5+qCK)Ai=o|DnU?Q%=e!UVCqZvNP?aTMTotXpz=O1JJp z-myI=PHGt7pj*Gr{*nl=P!S1Rh_%}`cucwxoci?m+#Eo_&j&#@Z6=# zb8V{geOu=GZRfl0%n#bmbsNmJzMeaO82wN9!iefZ)9d+e`TXSJPeYd%#tc4RFPwkj zHUHe;^Xt#^b2~qG)_uNzXW_l=!VTLm54U_-xV-R7^~*D_`FC5s{FHwgk$)b(^W{_c zmr2$60aY2s@H24a%Sd?4tUWSMf=E7y+vTb8@!&0*5u|Op7a9p)b4reD)C8y0Y!^JXBI>5E{48Y z3}0EK8UBbeTtv3P!f>mM2xv1Yiiv}IQ1+;G2e=Fa*~4peY4B?b;j+lW+6kEepn|ce z1R2gqvJK7xP7^OOLFB4oSUzQsp~zwai)R9;4KgKb)Bh|)kSxD9>z)|es3}h{RG!j) zohbSxJoBsO?yuT6zv@^{9=Zg2Aj;(*L zZT)RN0t=%}=EmT)P0%&~M6vsHQaWlOgXdB7%yGaW3JfPFDy@+yx|&dNc6}!SlnN7i z3G#w$w= zPrm`(7hh6#AXE(egtRGNo{GK~7a#f7@6ak6?U=oh(*cL|JqzJFQ8Pi6W?^y572h~$ zvo$r%T%G8~6CsT7W~@!zR*R}Lr%CCB7 z&OYU!PR5ASApBteWvB=dgdSbSP_RG@wdZ{v zsP>nyF`e2L?m&8Y+-7Tk?S}8Ek579R$&vaS7t==?*Sb2M-uNTq`K8TwdHS1vW=&iT z?(Jattv{w;oMvq_@`D=Qc%+K_P5Rca&eW(CIS_sJ{y7X=JH^+irytc~T4~2@Ve!k= ziT<5SHyIJd=7ooXuHMoL-hWkmN}K;`H>U`=N)3)hQTs{1cSMz( z2dxSWAD5`ns_^HkVksL4sx-Ysh9Wr>{FW1(fj)a7N-8qqk~(ppIq14kC<0D7nZo)n zd;lJ%+b^;+Q){K0X{ykqazdbv(*Q@4bkj*3;k^QnF@tMPPT6l6*=`Z(zj|9oWT0WO zQ&g};V@K3_yL(R2VXF_OMn`xfoMR#b&925oNBBF(#yTE<08_(gCBoFgi5W2UaLWkp z2IpZ$CyqQ0a_5~oWBX6#-5$H8lI|SA*$_7;UW9CNpw z+STutR_gKmM%o$0-FHtv%Qv4mqgdUzZ*NpTb%Qo=i0A&PKL5ncjEn#DyJuc9I=p={ ze#an8%iT)wAb2a?X(Jhprsn(DWr&s57pCZLf9GXx`LsU?#}c34~SY z(}=Y)H8LX$CT@i(D{1yGDR`u&OI+UdJlWJyLV{&aaR>YrwbUM}13O~3hKSj{pmbt+ z`;)s(dO5QHt=WxmQ7bV`AkK3U{i|Y{&=j|`e8Bvq5fBa~C6?JknSD@WNy(i)KR9N9 zqb&ah^2vB3xVnI&UPmmZFxepxIvBZMjG8W?K-nS|nmEe}6&WdSh~&yQ(&`qj;ux=} zN1IV!4K@=5UMIN>nZzMJBLRv2=eGC$aa9cZra#=|G(>pveeh zLmcQ5)6c6x`xzEQMGBhw5=&0ihQSDVeoA{q53LW1$UeT*AK`Kl*F1%dELco-_yZ5} zb9t7`%}sS`bQ`KTX`vKM^JR4`qDi?FST+r=mm!0g;sc?~Jh*N;0MYOEgFQIlNARUR zHjN>u2&lb^(ga$I(5}Q5q^bV|NjlAbF!TVGV2Oix&pyq5$V~@Lo4EL>XZ32F_x{c| zpO)8HK-GcKf1{GB=70G&9O^n9a^*j(6BCy8c0Zo^t$P?*o?3O@_1<=)#GBD|*J$V6 zZa0Uq++*qw|I-xc|LJMr#?f@l@e2{fEzcj10WF6sDVDg+j4Q!H^C~BE+X3IW{9%{X z;xCjC()Ot1H}SsCV11;2L-dq;{7qz-xjE5?wU{Pvi0yh3yYu(Oiz68{)v~jq>wi`; zya7m{`ACebj0wi22Y0#L380>rtF{Qs#+BkcNVfg3j*4r(RWa8`OS1BG>waYHe z?a$l{TO0r4`SWX*CfhXd&w?O-FYh!SR@M!mabx=+Dq&N7I-WH6%k3a6kA82pl@t{| zd{$IQN36!*>i@l?M?Fkw6KrQ+Nhtk!Ly)8wO%@l&j)Mx~brkv=jvtFF(L6kyVy@SH zEmZanMjn9MupTMd656lMX6V9_4!qPEInk}gBR+~rVw*diMf10xGQJp3QfUr@*HR#Q zS3pU3F#xSiXL-2c_?XaonDiSJFdny&qscwOI4n!;t(32yZltz>!clTLIuUoax_A@e ziDth9@JNkB8Uyi~RkEr6qat@3hQuI*al!y@yaBb>=ImaP^khI@fvQVR&DgTr@kJV( zJg_dYaF}PAG9ET&;-kjNL12XI*Z7bzDaWYTB47a zs97?7u#fBE-erk4DR9|q4lv6NeteM4$SSiOHQA&rU3`dI9H z?>pD*Q?`6R$??oe@*X_xrI(o)#IRABX2FAKT%^ks>q@9`Yna4L)wrSr(M;0R7W%=W z21+!lp25=@tq`xLV4XaMZdLof+k~sfbq*-0A`{J&w8h03u2kuWBncj~1Bmrv2h<9F zpr8Z+K-5wR#`mh$48sEFYyLI*;XI_BkTrDn;}8?7H0B``C~v&c30EAhLof6tE_ zN9rxPncjELS#8~~Fk$s5CB-J%P6j`82Hl2j3cm_dyJ%~Q#=U0@+KQ~RKI3kA4A?d8 zyL0MBto^F0kUB|8-~i<|%XRJBSA9;dm&nATFr{5j146FlJXzRjlhgZCy+o1S(A<-T z_x`C4W|+zc_3wZj((|w4loW>kr`ZGd$OZ={-i&TD$lEr8-1O_Y?W&|xUl49k0yfh! zY1InAGcnc_>?!C`e{PAilus38CQPeWS|a9;*>j6J-~LiW2Tb(7{+?Ku_0#Vq-(>WQ zV4LNj{t(UE_1n>cAE=%+9bfo%&%dCGB=Q5x1pbd`_zouiX*Bo8v*IsPw-v4Cv|HrP zXI()8M4=GMKm!a#&IS#pqJdp#V2hBQGyr3WU|S@xEtc#|8as&r>{{Y4 zd@}{Qa|z`*$kRH6(46N+E%AU(o_#Y)d4RVk{dTwj;kbzK8Rh{}q;rF()0&>g-}_7! zQH5ey#C(@iI<*oa*$%Q0fPc|ADrL4Si^e}7;058RI|uakfyk&Ce;X-x?A)zrY)xS^ zKU3^)JK8rih1$1-+-k|P&lMIjQ3r%PT?#dE@Z>=uV#|Cn_XT-NmA^}Am$4;#>}YRJ zsFiyzzCgeYo`oAS`2c_j7xRGOVt~j81jRw=#p(o~6geIipU(C-MFn#Ej5>=ggvHo& zqydNs4eh@}bu3^aoMr(GjU7y&7LjfTNg&E%i2rOr)fDOg2r*goE|j8fM5Ff1@(<2( zHx1s)l)>%AAchECLxgJ0cD2cPf<@GB8rxcoWN6h?FDf+TJzw&Jv(h2yW&H(WK7m3N zb?EF9B7-dTBh2=k7R3aVpa-U9*yWX$$# zo8jAvK++QbAzbHJivPt)lssYE)ITwGS;h_^;_jn)56ANw&3N8>LFd%R*G~nboP?`K_o@;r%0DpY`?unRS*}Ql0 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

c||457=vJzYLUT=E@<6Y&LNG3pHb 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 v`k=4-pkaa#bfD{ZpbKNrsXyo_AGF^A+F1i_KJZ { + await usingTempDir(async (cwd) => { + await run("npm", ["init", "--yes"], cwd); + await run("npm", ["install"], cwd); + await run(initBin, ["--defaults"], cwd); + + await run( + "npm", + ["run", "adr", "--", "new", "--quiet", '"E2E test ADR"'], + cwd + ); + + const adrListRes = await run( + "npm", + ["run", "adr", "--", "list", "--raw"], + cwd + ); + expect(adrListRes.stdout).to.contain( + "use-log4brains-to-manage-the-adrs", + "Log4brains ADR was not created by init" + ); + expect(adrListRes.stdout).to.contain( + "use-markdown-architectural-decision-records", + "MADR ADR was not created by init" + ); + expect(adrListRes.stdout).to.contain( + "E2E test ADR", + "E2E test ADR was not created" + ); + + // TODO: preview & build tests (https://github.com/thomvaill/log4brains/issues/2) + + console.log(chalk.bold.green("END")); + }); +})().catch((e) => { + console.error(""); + console.error(`${chalk.red.bold("== FATAL ERROR ==")}`); + console.error(e); + process.exit(1); +}); diff --git a/jest.config.base.js b/jest.config.base.js new file mode 100644 index 00000000..3f878c46 --- /dev/null +++ b/jest.config.base.js @@ -0,0 +1,4 @@ +module.exports = { + preset: "ts-jest", + testEnvironment: "node" +}; diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..6104fb47 --- /dev/null +++ b/lerna.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0-beta.0", + "npmClient": "yarn", + "useWorkspaces": true, + "packages": [ + "packages/*" + ], + "command": { + "version": { + "message": "chore(release): publish %s", + "allowBranch": "dev" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..72e4c855 --- /dev/null +++ b/package.json @@ -0,0 +1,82 @@ +{ + "name": "root", + "private": true, + "engines": { + "node": ">=10.23.0" + }, + "workspaces": [ + "packages/**" + ], + "scripts": { + "dev": "lerna run --parallel dev", + "build": "lerna run build", + "clean": "lerna run clean", + "typescript": "lerna run typescript", + "test": "lerna run --stream test", + "test:changed": "lerna run --stream --since HEAD test", + "lint": "lerna run --stream lint", + "format": "prettier-eslint \"$PWD/**/{.,}*.{js,jsx,ts,tsx,json,md}\" --list-different", + "format:fix": "yarn format --write", + "typedoc": "lerna run typedoc", + "adr": "./packages/cli/dist/log4brains adr", + "log4brains-preview": "./packages/web/dist/bin/log4brains-web preview", + "log4brains-preview:dev": "cross-env NODE_ENV=development yarn log4brains-preview", + "log4brains-build": "./packages/web/dist/bin/log4brains-web build", + "serve": "serve .log4brains/out", + "links": "lerna run --stream link", + "e2e": "node e2e-tests/e2e-launcher.js", + "lerna": "lerna" + }, + "devDependencies": { + "@commitlint/cli": "^11.0.0", + "@commitlint/config-conventional": "^11.0.0", + "@types/jest": "^26.0.14", + "@types/node": "^14.11.2", + "@types/rimraf": "^3.0.0", + "@typescript-eslint/eslint-plugin": "^4.2.0", + "@typescript-eslint/parser": "^4.2.0", + "chai": "^4.2.0", + "chalk": "^4.1.0", + "cross-env": "^7.0.2", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^7.9.0", + "eslint-config-airbnb-typescript": "^10.0.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jest": "^24.0.2", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-react": "^7.20.6", + "eslint-plugin-react-hooks": "^4.1.2", + "eslint-plugin-sonarjs": "^0.5.0", + "execa": "^4.1.0", + "husky": "^4.3.5", + "jest": "^26.4.2", + "jest-mock-extended": "^1.0.10", + "lerna": "^3.22.1", + "lint-staged": "^10.5.3", + "nodemon": "^2.0.6", + "prettier": "^2.1.2", + "prettier-eslint-cli": "^5.0.0", + "rimraf": "^3.0.2", + "serve": "^11.3.2", + "ts-jest": "^26.4.0", + "typedoc": "0.17.0-3", + "typescript": "^4.0.3" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + }, + "lint-staged": { + "*.{ts,tsx}": "eslint --max-warnings=0", + "*.{js,jsx,ts,tsx,json,md}": "prettier-eslint --list-different" + }, + "config": { + "commitizen": { + "path": "cz-conventional-changelog" + } + } +} diff --git a/packages/cli-common/.eslintrc.js b/packages/cli-common/.eslintrc.js new file mode 100644 index 00000000..1f6675ff --- /dev/null +++ b/packages/cli-common/.eslintrc.js @@ -0,0 +1,11 @@ +const path = require("path"); + +module.exports = { + env: { + node: true + }, + parserOptions: { + project: path.join(__dirname, "tsconfig.json") + }, + extends: ["../../.eslintrc"] +}; diff --git a/packages/cli-common/README.md b/packages/cli-common/README.md new file mode 100644 index 00000000..b237d77b --- /dev/null +++ b/packages/cli-common/README.md @@ -0,0 +1,18 @@ +# @log4brains/cli-common + +This package provides common features for all [Log4brains](https://github.com/thomvaill/log4brains) CLI-based packages. +It is not meant to be used directly in your project. + +## Installation + +This package is not meant to be installed directly in your project. This is a common dependency of [@log4brains/cli](https://www.npmjs.com/package/@log4brains/cli), [@log4brains/init](https://www.npmjs.com/package/@log4brains/init) and [@log4brains/web](https://www.npmjs.com/package/@log4brains/web), which is installed automatically. + +## Development + +```bash +yarn dev:test +``` + +## Documentation + +- [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md) diff --git a/packages/cli-common/dev-tests/run.ts b/packages/cli-common/dev-tests/run.ts new file mode 100644 index 00000000..3bccf0ee --- /dev/null +++ b/packages/cli-common/dev-tests/run.ts @@ -0,0 +1,137 @@ +/* eslint-disable no-console */ +import chalk from "chalk"; +import { AppConsole } from "../src/AppConsole"; + +function sleep(seconds: number) { + return new Promise((resolve) => { + setTimeout(resolve, seconds * 1000); + }); +} + +async function t(name: string, cb: () => void | Promise): Promise { + console.log(chalk.dim(`*** TEST: ${name}`)); + await cb(); + console.log(chalk.dim(`*** END`)); + console.log(); +} + +/** + * Visual tests for AppConsole + */ +void (async () => { + await t("print()", () => { + const appConsole = new AppConsole(); + appConsole.println("Line 1"); + appConsole.println("Line 2"); + appConsole.println(); + appConsole.println("Line 3 with new line"); + }); + + await t("debug() off", () => { + const appConsole = new AppConsole(); + appConsole.debug("This should not be printed"); + }); + + await t("debug() on", () => { + const appConsole = new AppConsole({ debug: true }); + appConsole.debug("Line 1"); + appConsole.debug("Line 2"); + }); + + await t("warn() with message", () => { + const appConsole = new AppConsole(); + appConsole.warn("This is a warning"); + appConsole.warn("This is a warning line2"); + }); + + await t("warn() with Error", () => { + const appConsole = new AppConsole(); + appConsole.warn(new Error("The message or the generic error")); + appConsole.warn(new RangeError("The message or the RangeError")); + }); + + await t("warn() with Error and traces on", () => { + const appConsole = new AppConsole({ traces: true }); + appConsole.warn(new Error("The message or the generic error")); + appConsole.warn(new RangeError("The message or the RangeError")); + }); + + await t("error() with message", () => { + const appConsole = new AppConsole(); + appConsole.error("This is an error"); + appConsole.error("This is an error line2"); + }); + + await t("error() with Error", () => { + const appConsole = new AppConsole(); + appConsole.error(new Error("The message or the generic error")); + appConsole.error(new RangeError("The message or the RangeError")); + }); + + await t("error() with Error and traces on", () => { + const appConsole = new AppConsole({ traces: true }); + appConsole.error(new Error("The message or the generic error")); + appConsole.error(new RangeError("The message or the RangeError")); + }); + + await t("fatal() with message", () => { + const appConsole = new AppConsole(); + appConsole.fatal("This is an error"); + appConsole.fatal("This is an error line2"); + }); + + await t("fatal() with Error", () => { + const appConsole = new AppConsole(); + appConsole.fatal(new Error("The message or the generic error")); + appConsole.fatal(new RangeError("The message or the RangeError")); + }); + + await t("fatal() with Error and traces on", () => { + const appConsole = new AppConsole({ traces: true }); + appConsole.fatal(new Error("The message or the generic error")); + appConsole.fatal(new RangeError("The message or the RangeError")); + }); + + await t("success()", () => { + const appConsole = new AppConsole(); + appConsole.success("Yeah! This is a success!"); + }); + + await t("table", () => { + const appConsole = new AppConsole(); + const table = appConsole.createTable({ head: ["Col 1", "Col 2"] }); + table.push(["Cell 1.1", "Cell 1.2"]); + table.push(["Cell 2.1", "Cell 2.2"]); + appConsole.printTable(table); + appConsole.printTable(table, true); + }); + + await t("askYesNoQuestion()", async () => { + const appConsole = new AppConsole(); + await appConsole.askYesNoQuestion("Do you like this script?", true); + }); + + await t("askInputQuestion()", async () => { + const appConsole = new AppConsole(); + await appConsole.askInputQuestion( + "Please enter something", + "default value" + ); + }); + + await t("askListQuestion()", async () => { + const appConsole = new AppConsole(); + await appConsole.askListQuestion("Please select something", [ + { name: "Option 1", value: "opt1", short: "O1" }, + { name: "Option 2", value: "opt2", short: "O2" } + ]); + }); + + await t("spinner", async () => { + const appConsole = new AppConsole(); + appConsole.startSpinner("The spinner is spinning..."); + await sleep(5); + appConsole.stopSpinner(); + appConsole.success("This is a success!"); + }); +})(); diff --git a/packages/cli-common/nodemon.json b/packages/cli-common/nodemon.json new file mode 100644 index 00000000..002240a7 --- /dev/null +++ b/packages/cli-common/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["src", "dev-tests"], + "ext": "ts", + "exec": "clear && node -r esm -r ts-node/register ./dev-tests/run.ts" +} diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json new file mode 100644 index 00000000..a7a4cce2 --- /dev/null +++ b/packages/cli-common/package.json @@ -0,0 +1,52 @@ +{ + "name": "@log4brains/cli-common", + "version": "1.0.0-beta.0", + "description": "Log4brains architecture knowledge base common CLI features", + "keywords": [ + "log4brains" + ], + "author": "Thomas Vaillant ", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/thomvaill/log4brains", + "repository": { + "type": "git", + "url": "https://github.com/thomvaill/log4brains", + "directory": "packages/cli-common" + }, + "engines": { + "node": ">=10.23.0" + }, + "files": [ + "dist" + ], + "source": "./src/index.ts", + "main": "./dist/index.js", + "module": "./dist/index.module.js", + "types": "./dist/index.d.ts", + "scripts": { + "dev": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node watch", + "dev:test": "nodemon", + "build": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node", + "clean": "rimraf ./dist", + "typescript": "tsc --noEmit", + "lint": "eslint . --max-warnings=0", + "prepublishOnly": "yarn build", + "link": "npm link && rm -f ./package-lock.json" + }, + "devDependencies": { + "@types/inquirer": "^7.3.1", + "esm": "^3.2.25", + "nodemon": "^2.0.6", + "ts-node": "^9.1.1" + }, + "dependencies": { + "chalk": "^4.1.0", + "cli-table3": "^0.6.0", + "inquirer": "^7.3.3", + "ora": "^5.1.0" + } +} diff --git a/packages/cli-common/src/AppConsole.ts b/packages/cli-common/src/AppConsole.ts new file mode 100644 index 00000000..7f626b20 --- /dev/null +++ b/packages/cli-common/src/AppConsole.ts @@ -0,0 +1,212 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable no-console */ +/* eslint-disable class-methods-use-this */ +import chalk from "chalk"; +import inquirer from "inquirer"; +import ora, { Ora } from "ora"; +import CliTable3, { Table } from "cli-table3"; +import { ConsoleCapturer } from "./ConsoleCapturer"; + +export type AppConsoleOptions = { + debug: boolean; + traces: boolean; +}; + +export type ChoiceDefinition = { + name: string; + value: V; + short?: string; +}; + +export class AppConsole { + private readonly opts: AppConsoleOptions; + + private spinner?: Ora; + + private spinnerConsoleCapturer = new ConsoleCapturer(); + + constructor(opts: Partial = {}) { + this.opts = { + debug: false, + traces: false, + ...opts + }; + } + + isSpinning(): boolean { + return !!this.spinner; + } + + startSpinner(message: string): void { + if (this.spinner) { + throw new Error("Spinner already started"); + } + this.spinner = ora({ + text: message, + spinner: "bouncingBar", + stream: process.stdout + }).start(); + + // Add capturing of console.log/warn/error to allow pausing + // the spinner before logging and then restarting spinner after + this.spinnerConsoleCapturer.onLog = (method, args) => { + this.spinner?.stop(); + method(...args); + this.spinner?.start(); + }; + this.spinnerConsoleCapturer.start(); + } + + updateSpinner(message: string): void { + if (!this.spinner) { + throw new Error("Spinner is not started"); + } + this.spinner.text = message; + } + + stopSpinner(withError = false): void { + if (!this.spinner) { + throw new Error("Spinner is not started"); + } + + this.spinnerConsoleCapturer.stop(); + + this.spinner.stopAndPersist({ + symbol: chalk.dim(withError ? "[== ]" : "[====]"), + text: `${this.spinner.text} ${withError ? chalk.red("Error") : "Done"}` + }); + this.println(); + this.spinner = undefined; + } + + println(message?: any, ...optionalParams: any[]): void { + console.log(message ?? "", ...optionalParams); + } + + printlnErr(message?: any, ...optionalParams: any[]): void { + console.error(message ?? "", ...optionalParams); + } + + debug(message?: any, ...optionalParams: any[]): void { + if (this.opts.debug) { + this.println( + chalk.dim(message), + ...optionalParams.map((p) => chalk.dim(p)) + ); + } + } + + warn(messageOrErr: string | Error): void { + if (messageOrErr instanceof Error) { + this.printlnErr(chalk.yellowBright(` ⚠ ${messageOrErr.message}`)); + if (this.opts.traces && messageOrErr.stack) { + this.printlnErr(chalk.yellow(messageOrErr.stack)); + this.printlnErr(); + } + } else { + this.printlnErr(chalk.yellowBright(` ⚠ ${messageOrErr}`)); + } + } + + error(messageOrErr: string | Error): void { + if (messageOrErr instanceof Error) { + this.printlnErr(chalk.redBright(` ✖ ${messageOrErr.message}`)); + if (this.opts.traces && messageOrErr.stack) { + this.printlnErr(chalk.red(messageOrErr.stack)); + this.printlnErr(); + } + } else { + this.printlnErr(chalk.redBright(` ✖ ${messageOrErr}`)); + } + } + + fatal(messageOrErr: string | Error): void { + this.printlnErr(); + if (messageOrErr instanceof Error) { + this.printlnErr( + `${chalk.bgRed.bold(" FATAL ")} ${chalk.redBright( + messageOrErr.message + )}` + ); + if (this.opts.traces && messageOrErr.stack) { + this.printlnErr(); + this.printlnErr(chalk.red(messageOrErr.stack)); + } + } else { + this.printlnErr( + `${chalk.bgRed.bold(" FATAL ")} ${chalk.redBright(messageOrErr)}` + ); + } + this.printlnErr(); + } + + success(message: string): void { + this.println(chalk.greenBright(` ✔ ${message}`)); + } + + createTable(options?: CliTable3.TableConstructorOptions): Table { + return new CliTable3({ + style: { + head: ["blue"] + }, + ...options + }); + } + + printTable(table: Table, raw = false): void { + if (raw) { + table.forEach((value) => { + if (typeof value === "object" && value instanceof Array) { + console.log(value.join(",")); + } else { + console.log(value); + } + }); + } else { + console.log(table.toString()); + } + } + + async askYesNoQuestion( + question: string, + defaultValue: boolean + ): Promise { + const answer = await inquirer.prompt<{ q: boolean }>([ + { type: "confirm", name: "q", message: question, default: defaultValue } + ]); + return answer.q; + } + + async askInputQuestion( + question: string, + defaultValue?: string + ): Promise { + const answer = await inquirer.prompt<{ q: string }>([ + { + type: "input", + name: "q", + message: question, + default: defaultValue + } + ]); + return answer.q; + } + + async askListQuestion( + question: string, + choices: ChoiceDefinition[], + defaultValue?: V + ): Promise { + const answer = await inquirer.prompt<{ q: V }>([ + { + type: "list", + name: "q", + message: question, + default: defaultValue, + choices + } + ]); + return answer.q; + } +} diff --git a/packages/cli-common/src/ConsoleCapturer.ts b/packages/cli-common/src/ConsoleCapturer.ts new file mode 100644 index 00000000..e3e4b7b1 --- /dev/null +++ b/packages/cli-common/src/ConsoleCapturer.ts @@ -0,0 +1,82 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ + +export type ConsoleLogMethod = typeof console.log; +export type ConsoleWarnMethod = typeof console.warn; +export type ConsoleErrorMethod = typeof console.error; +export type ConsoleMethod = + | ConsoleLogMethod + | ConsoleWarnMethod + | ConsoleErrorMethod; + +/** + * Captures console.log(), console.error() and console.warn() + * Source: https://github.com/vercel/next.js/blob/canary/packages/next/build/spinner.ts Thanks! + */ +export class ConsoleCapturer { + private origConsoleLog?: ConsoleLogMethod; + + private origConsoleWarn?: ConsoleWarnMethod; + + private origConsoleError?: ConsoleErrorMethod; + + onLog?: ( + method: ConsoleMethod, + args: any[], + stream: "stdout" | "stderr" + ) => void; + + start(): void { + this.origConsoleLog = console.log; + this.origConsoleWarn = console.warn; + this.origConsoleError = console.error; + + const logHandle = (method: ConsoleMethod, args: any[]) => { + if (this.onLog) { + this.onLog( + method, + args, + method === this.origConsoleLog ? "stdout" : "stderr" + ); + } + }; + + console.log = (...args: any) => logHandle(this.origConsoleLog!, args); + console.warn = (...args: any) => logHandle(this.origConsoleWarn!, args); + console.error = (...args: any) => logHandle(this.origConsoleError!, args); + } + + doPrintln(message?: any, ...optionalParams: any[]): void { + if (!this.origConsoleLog) { + throw new Error("ConsoleCapturer is not started"); + } + this.origConsoleLog(message ?? "", ...optionalParams); + } + + doPrintlnErr(message?: any, ...optionalParams: any[]): void { + if (!this.origConsoleError) { + throw new Error("ConsoleCapturer is not started"); + } + this.origConsoleError(message ?? "", ...optionalParams); + } + + stop(): void { + if ( + !this.origConsoleLog || + !this.origConsoleWarn || + !this.origConsoleError + ) { + throw new Error("ConsoleCapturer is not started"); + } + + console.log = this.origConsoleLog; + console.warn = this.origConsoleWarn; + console.error = this.origConsoleError; + + this.origConsoleLog = undefined; + this.origConsoleWarn = undefined; + this.origConsoleError = undefined; + } +} diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts new file mode 100644 index 00000000..10d66c81 --- /dev/null +++ b/packages/cli-common/src/index.ts @@ -0,0 +1,2 @@ +export * from "./AppConsole"; +export * from "./ConsoleCapturer"; diff --git a/packages/cli-common/tsconfig.build.json b/packages/cli-common/tsconfig.build.json new file mode 100644 index 00000000..43e2a2b9 --- /dev/null +++ b/packages/cli-common/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dev-tests", "**/*.test.ts"] +} diff --git a/packages/cli-common/tsconfig.json b/packages/cli-common/tsconfig.json new file mode 100644 index 00000000..b3313b7b --- /dev/null +++ b/packages/cli-common/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@src/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "dev-tests/**/*.ts"], + "exclude": ["node_modules"], + "ts-node": { "files": true } +} diff --git a/packages/cli/.eslintrc.js b/packages/cli/.eslintrc.js new file mode 100644 index 00000000..1f6675ff --- /dev/null +++ b/packages/cli/.eslintrc.js @@ -0,0 +1,11 @@ +const path = require("path"); + +module.exports = { + env: { + node: true + }, + parserOptions: { + project: path.join(__dirname, "tsconfig.json") + }, + extends: ["../../.eslintrc"] +}; diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 00000000..231d2c12 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,49 @@ +# @log4brains/cli + +This package provides the CLI to use the [Log4brains](https://github.com/thomvaill/log4brains) architecture knowledge base in your project. + +## Installation + +You should use `npx init-log4brains` as described in the [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md), which will install all the required dependencies in your project, including this one, and set up the right scripts in your `package.json`. + +You can also install this package manually via npm or yarn: + +```bash +npm install --save-dev @log4brains/cli +``` + +or + +```bash +yarn add --dev @log4brains/cli +``` + +And add this script to your `package.json`: + +```json +{ + [...] + "scripts": { + [...] + "adr": "log4brains adr" + } +} +``` + +## Usage + +See [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md), or run this command in your project: + +```bash +npm run adr -- --help +``` + +or + +```bash +yarn adr --help +``` + +## Documentation + +- [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md) diff --git a/packages/cli/nodemon.json b/packages/cli/nodemon.json new file mode 100644 index 00000000..e9329fd9 --- /dev/null +++ b/packages/cli/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["src"], + "ext": "ts", + "exec": "yarn build" +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..dd0ba78c --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,50 @@ +{ + "name": "@log4brains/cli", + "version": "1.0.0-beta.0", + "description": "Log4brains architecture knowledge base CLI", + "keywords": [ + "log4brains" + ], + "author": "Thomas Vaillant ", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/thomvaill/log4brains", + "repository": { + "type": "git", + "url": "https://github.com/thomvaill/log4brains", + "directory": "packages/cli" + }, + "engines": { + "node": ">=10.23.0" + }, + "files": [ + "dist" + ], + "bin": { + "log4brains": "./dist/log4brains" + }, + "scripts": { + "dev": "nodemon", + "build": "tsc --build tsconfig.build.json && copyfiles -u 1 src/log4brains dist", + "clean": "rimraf ./dist", + "typescript": "tsc --noEmit", + "lint": "eslint . --max-warnings=0", + "prepublishOnly": "yarn build", + "link": "yarn link" + }, + "dependencies": { + "@log4brains/cli-common": "^1.0.0-beta.0", + "@log4brains/core": "^1.0.0-beta.0", + "commander": "^6.1.0", + "esm": "^3.2.25", + "execa": "^5.0.0", + "has-yarn": "^2.1.0", + "terminal-link": "^2.1.1" + }, + "devDependencies": { + "copyfiles": "^2.4.0" + } +} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 00000000..d2ea7d94 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,73 @@ +import commander from "commander"; +import { Log4brains } from "@log4brains/core"; +import type { AppConsole } from "@log4brains/cli-common"; +import { + ListCommand, + ListCommandOpts, + NewCommand, + NewCommandOpts +} from "./commands"; + +type Deps = { + l4bInstance: Log4brains; + appConsole: AppConsole; + version: string; +}; + +export function createCli({ + l4bInstance, + appConsole, + version +}: Deps): commander.Command { + const program = new commander.Command(); + program.version(version); + + const adr = program + .command("adr") + .description("Manage the Architecture Decision Records (ADR)"); + + adr + .command("new [title]") + .description("Create an ADR", { + title: "The title of the ADR. Required if --quiet is passed" + }) + .option("-q, --quiet", "Disable interactive mode", false) + .option( + "-p, --package ", + "To create the ADR for a specific package" + ) + .option( + "--from ", + "Copy contents into the ADR instead of using the default template" + ) + .action( + (title: string | undefined, opts: NewCommandOpts): Promise => { + return new NewCommand({ l4bInstance, appConsole }).execute(opts, title); + } + ); + + // adr + // .command("quick") + // .description("Create a one-sentence ADR (Y-Statement)") + // .action( + // (): Promise => { + // // TODO + // } + // ); + + adr + .command("list") + .option( + "-s, --statuses ", + "Filter on the given statuses, comma-separated" + ) // TODO: list available statuses + .option("-r, --raw", "Use a raw format instead of a table", false) + .description("List ADRs") + .action( + (opts: ListCommandOpts): Promise => { + return new ListCommand({ l4bInstance, appConsole }).execute(opts); + } + ); + + return program; +} diff --git a/packages/cli/src/commands/ListCommand.ts b/packages/cli/src/commands/ListCommand.ts new file mode 100644 index 00000000..7c7a62cf --- /dev/null +++ b/packages/cli/src/commands/ListCommand.ts @@ -0,0 +1,43 @@ +import { Log4brains, SearchAdrsFilters, AdrDtoStatus } from "@log4brains/core"; +import type { AppConsole } from "@log4brains/cli-common"; + +type Deps = { + l4bInstance: Log4brains; + appConsole: AppConsole; +}; + +export type ListCommandOpts = { + statuses: string; + raw: boolean; +}; + +export class ListCommand { + private readonly l4bInstance: Log4brains; + + private readonly console: AppConsole; + + constructor({ l4bInstance, appConsole }: Deps) { + this.l4bInstance = l4bInstance; + this.console = appConsole; + } + + async execute(opts: ListCommandOpts): Promise { + const filters: SearchAdrsFilters = {}; + if (opts.statuses) { + filters.statuses = opts.statuses.split(",") as AdrDtoStatus[]; + } + const adrs = await this.l4bInstance.searchAdrs(filters); + const table = this.console.createTable({ + head: ["Slug", "Status", "Package", "Title"] + }); + adrs.forEach((adr) => { + table.push([ + adr.slug, + adr.status.toUpperCase(), + adr.package || "", + adr.title || "Untitled" + ]); + }); + this.console.printTable(table, opts.raw); + } +} diff --git a/packages/cli/src/commands/NewCommand.ts b/packages/cli/src/commands/NewCommand.ts new file mode 100644 index 00000000..cedb2e29 --- /dev/null +++ b/packages/cli/src/commands/NewCommand.ts @@ -0,0 +1,172 @@ +/* eslint-disable no-await-in-loop */ +import path from "path"; +import { Log4brains, Log4brainsError } from "@log4brains/core"; +import fs, { promises as fsP } from "fs"; +import type { AppConsole } from "@log4brains/cli-common"; +import { previewAdr } from "../utils"; + +type Deps = { + l4bInstance: Log4brains; + appConsole: AppConsole; +}; + +export type NewCommandOpts = { + quiet: boolean; + package?: string; + from?: string; +}; + +export class NewCommand { + private readonly l4bInstance: Log4brains; + + private readonly console: AppConsole; + + constructor({ l4bInstance, appConsole }: Deps) { + this.l4bInstance = l4bInstance; + this.console = appConsole; + } + + private detectCurrentPackageFromCwd(): string | undefined { + const { packages } = this.l4bInstance.config.project; + if (!packages) { + return undefined; + } + const cwd = path.resolve("."); + const match = packages + .filter((pkg) => cwd.includes(pkg.path)) + .sort((a, b) => a.path.length - b.path.length) + .pop(); // returns the most precise path (ie. longest) + return match?.name; + } + + // eslint-disable-next-line sonarjs/cognitive-complexity + async execute(opts: NewCommandOpts, titleArg?: string): Promise { + const { packages } = this.l4bInstance.config.project; + + let pkg = opts.package; + if (!opts.quiet && !pkg && packages && packages.length > 0) { + const currentPackage = this.detectCurrentPackageFromCwd(); + const packageChoices = [ + { + name: `Global`, + value: "" + }, + ...packages.map((p) => ({ + name: `Package: ${p.name}`, + value: p.name + })) + ]; + pkg = + (await this.console.askListQuestion( + "For which package do you want to create this new ADR?", + packageChoices, + currentPackage + )) || undefined; + } + + if (opts.quiet && !titleArg) { + throw new Log4brainsError(" is required when using --quiet"); + } + let title; + do { + title = + titleArg || + (await this.console.askInputQuestion( + "Title of the solved problem and its solution?" + )); + if (!title.trim()) { + this.console.warn("Please enter a title"); + } + } while (!title.trim()); + + // const slug = await this.console.askInputQuestion( + // "We pre-generated a slug to identify this ADR. Press [ENTER] or enter another one.", + // await this.l4bInstance.generateAdrSlug(title, pkg) + // ); + const slug = await this.l4bInstance.generateAdrSlug(title, pkg); + + const adrDto = await this.l4bInstance.createAdrFromTemplate(slug, title); + + // --from option (used by init-log4brains to create the starter ADRs) + // Since this is a private use case, we don't include it in CORE for now + if (opts.from) { + if (!fs.existsSync(opts.from)) { + throw new Log4brainsError("The given file does not exist", opts.from); + } + // TODO: use streams + await fsP.writeFile( + adrDto.file.absolutePath, + await fsP.readFile(opts.from, "utf-8"), + "utf-8" + ); + } + + if (opts.quiet) { + this.console.println(adrDto.slug); + process.exit(0); + } + + const activeAdrs = await this.l4bInstance.searchAdrs({ + statuses: ["accepted"] + }); + if (activeAdrs.length > 0) { + const supersedeChoices = [ + { + name: "No", + value: "" + }, + ...activeAdrs.map((a) => ({ + name: a.title || "Untitled", // TODO: add package and maybe date + format with tabs + value: a.slug + })) + ]; + const supersededSlug = await this.console.askListQuestion( + "Does this ADR supersede a previous one?", + supersedeChoices, + "" + ); + + if (supersededSlug !== "") { + await this.l4bInstance.supersedeAdr(supersededSlug, slug); + this.console.debug( + `${supersededSlug} was marked as superseded by ${slug}` + ); + } + } + + this.console.println(); + this.console.success(`New ADR created: ${adrDto.file.relativePath}`); + this.console.println(); + + const actionChoices = [ + { + name: "Edit and preview", + value: "edit-and-preview" + }, + { name: "Edit", value: "edit" }, + { name: "Later", value: "close" } + ]; + const action = await this.console.askListQuestion( + "How would you like to edit it?", + actionChoices, + "edit-and-preview" + ); + + if (action === "edit-and-preview" || action === "edit") { + await this.l4bInstance.openAdrInEditor(slug, () => { + this.console.warn( + "We were not able to detect your preferred editor :(" + ); + this.console.warn( + "You can define it by setting your $VISUAL or $EDITOR environment variable in ~/.zshenv or ~/.bashrc" + ); + }); + + if (action === "edit-and-preview") { + await previewAdr(slug); + } + } + + process.exit(0); + } +} diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts new file mode 100644 index 00000000..f1e30848 --- /dev/null +++ b/packages/cli/src/commands/index.ts @@ -0,0 +1,2 @@ +export * from "./ListCommand"; +export * from "./NewCommand"; diff --git a/packages/cli/src/log4brains b/packages/cli/src/log4brains new file mode 100755 index 00000000..05abe630 --- /dev/null +++ b/packages/cli/src/log4brains @@ -0,0 +1,5 @@ +#!/usr/bin/env node +require = require("esm")(module, { + mainFields: ["module", "main"] +}); +module.exports = require("./main"); diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts new file mode 100644 index 00000000..cbec4d68 --- /dev/null +++ b/packages/cli/src/main.ts @@ -0,0 +1,55 @@ +import fs from "fs"; +import path from "path"; +import terminalLink from "terminal-link"; +import { Log4brains, Log4brainsError } from "@log4brains/core"; +import { AppConsole } from "@log4brains/cli-common"; +import { createCli } from "./cli"; + +const templateExampleUrl = + "https://raw.githubusercontent.com/thomvaill/log4brains/master/packages/init/assets/template.md"; + +function findRootFolder(cwd: string): string { + if (fs.existsSync(path.join(cwd, ".log4brains.yml"))) { + return cwd; + } + if (path.resolve(cwd) === "/") { + throw new Error("Impossible to find a .log4brains.yml configuration file"); + } + return findRootFolder(path.join(cwd, "..")); +} + +const debug = !!process.env.DEBUG; +const dev = process.env.NODE_ENV === "development"; +const appConsole = new AppConsole({ debug, traces: debug || dev }); + +try { + // eslint-disable-next-line + const pkgVersion = require("../package.json").version as string; + + const l4bInstance = Log4brains.create( + findRootFolder(process.env.LOG4BRAINS_CWD || ".") + ); + + const cli = createCli({ version: pkgVersion, l4bInstance, appConsole }); + cli.parseAsync(process.argv).catch((err) => { + appConsole.fatal(err); + + if ( + err instanceof Log4brainsError && + err.name === "The template.md file does not exist" + ) { + appConsole.printlnErr( + `You can use this ${terminalLink( + "template", + templateExampleUrl + )} as an example` + ); + appConsole.printlnErr(); + } + + process.exit(1); + }); +} catch (e) { + appConsole.fatal(e); + process.exit(1); +} diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts new file mode 100644 index 00000000..d70e7568 --- /dev/null +++ b/packages/cli/src/utils.ts @@ -0,0 +1,15 @@ +import hasYarn from "has-yarn"; +import execa from "execa"; + +export async function previewAdr(slug: string): Promise<void> { + const subprocess = hasYarn() + ? execa("yarn", ["run", "log4brains-preview", slug], { + stdio: "inherit" + }) + : execa("npm", ["run", "--silent", "log4brains-preview", "--", slug], { + stdio: "inherit" + }); + subprocess.stdout?.pipe(process.stdout); + subprocess.stderr?.pipe(process.stderr); + await subprocess; +} diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json new file mode 100644 index 00000000..d4f56f83 --- /dev/null +++ b/packages/cli/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000..8de03f52 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "baseUrl": ".", + "paths": { + "@src/*": ["src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"], + "ts-node": { "files": true } +} diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js new file mode 100644 index 00000000..1f6675ff --- /dev/null +++ b/packages/core/.eslintrc.js @@ -0,0 +1,11 @@ +const path = require("path"); + +module.exports = { + env: { + node: true + }, + parserOptions: { + project: path.join(__dirname, "tsconfig.json") + }, + extends: ["../../.eslintrc"] +}; diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..075f5320 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,36 @@ +# @log4brains/core + +This package provides the core API of the [Log4brains](https://github.com/thomvaill/log4brains) architecture knowledge base. +It is not meant to be used directly in your project. + +## Installation + +This package is not meant to be installed directly in your project. This is a common dependency of [@log4brains/cli](https://www.npmjs.com/package/@log4brains/cli) and [@log4brains/web](https://www.npmjs.com/package/@log4brains/web), which is installed automatically. + +However, if you want to create a package to extend [Log4brains](https://github.com/thomvaill/log4brains)' capabilities, +you can include this package as a dependency of yours via npm or yarn: + +```bash +npm install --save @log4brains/core +``` + +or + +```bash +yarn add @log4brains/core +``` + +## Usage + +```typescript +import { Log4brains } from "@log4brains/core"; + +const l4b = Log4brains.create(process.cwd()); + +// See the TypeDoc documentation (TODO: to deploy on GitHub pages) to see available API methods +``` + +## Documentation + +- TypeDoc documentation (TODO) +- [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md) diff --git a/packages/core/docs/.gitignore b/packages/core/docs/.gitignore new file mode 100644 index 00000000..9b470da0 --- /dev/null +++ b/packages/core/docs/.gitignore @@ -0,0 +1 @@ +typedoc diff --git a/packages/core/docs/adr/20201002-use-explicit-architecture-and-ddd-for-the-core-api.md b/packages/core/docs/adr/20201002-use-explicit-architecture-and-ddd-for-the-core-api.md new file mode 100644 index 00000000..e9ad3d2a --- /dev/null +++ b/packages/core/docs/adr/20201002-use-explicit-architecture-and-ddd-for-the-core-api.md @@ -0,0 +1,9 @@ +# Use Explicit Architecture and DDD for the core API + +- Status: accepted +- Date: 2020-10-02 + +As mentioned in [20200925-multi-packages-architecture-in-a-monorepo-with-yarn-and-lerna](../../../../docs/adr/20200925-multi-packages-architecture-in-a-monorepo-with-yarn-and-lerna.md), we want the core API to be well-tested because all the business logic will happen here. + +Herberto Graça did an awesome job but by putting together all the best practices of DDD, hexagonal architecture, onion architecture, clean architecture, CQRS... in what he calls [Explicit Architecture](https://herbertograca.com/tag/explicit-architecture/). +We will use this architecture for our core package. diff --git a/packages/core/docs/adr/20201003-markdown-parsing-is-part-of-the-domain.md b/packages/core/docs/adr/20201003-markdown-parsing-is-part-of-the-domain.md new file mode 100644 index 00000000..70a0df47 --- /dev/null +++ b/packages/core/docs/adr/20201003-markdown-parsing-is-part-of-the-domain.md @@ -0,0 +1,26 @@ +# Markdown parsing is part of the domain + +- Status: accepted +- Date: 2020-10-03 + +## Context and Problem Statement + +Development of the core domain. + +## Considered Options + +- Markdown is part of the domain +- Markdown is a technical detail, which should be developed in the infrastructure layer + +## Decision Outcome + +Chosen option: "Markdown is part of the domain" because we want to be able to parse it "smartly", without forcing a specific structure. +Therefore, a lot of business logic is involved and should be tested. + +### Positive Consequences + +- Test coverage of the markdown parsing + +### Negative Consequences + +- The business logic is tightly tied to the markdown format. It won't be possible to switch to another format easily in the future diff --git a/packages/core/docs/adr/20201027-adr-link-resolver-in-the-domain.md b/packages/core/docs/adr/20201027-adr-link-resolver-in-the-domain.md new file mode 100644 index 00000000..f15253ac --- /dev/null +++ b/packages/core/docs/adr/20201027-adr-link-resolver-in-the-domain.md @@ -0,0 +1,21 @@ +# ADR link resolver in the domain + +- Status: accepted +- Date: 2020-10-27 + +## Context and Problem Statement + +We have to translate markdown links between ADRs to static site links. +We cannot deduce the slug easily from the paths because of the "path / package" mapping, which is only known from the config. + +## Considered Options + +- Option 1: the ADR repository sets a Map on the ADR (`path -> slug`) for every link discovered in the Markdown +- Option 2: we introduce an "ADR link resolver" in the domain +- Option 3: we don't rely on link paths, but only on link labels (ie label must === slug) + +## Decision Outcome + +Chosen option: "Option 2: we introduce an "ADR link resolver" in the domain". +Because option 3 is too restrictive and option 1 seems too hacky. +And this solution is compatible with [20201003-markdown-parsing-is-part-of-the-domain](20201003-markdown-parsing-is-part-of-the-domain.md). diff --git a/packages/core/integration-tests/__snapshots__/ro.test.ts.snap b/packages/core/integration-tests/__snapshots__/ro.test.ts.snap new file mode 100644 index 00000000..aa6f30b6 --- /dev/null +++ b/packages/core/integration-tests/__snapshots__/ro.test.ts.snap @@ -0,0 +1,976 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`E2E tests / RO getAdrBySlug() existing ADR 1`] = ` +Object { + "body": Object { + "enhancedMdx": " +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# First ADR + +- Status: accepted +- Deciders: John Doe, Foo bar +- Date: 2020-01-01 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [ + "John Doe", + "Foo bar", + ], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20200101-first-adr.md", + "relativePath": "docs/adr/20200101-first-adr.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-01-01T22:59:59.000Z", + "slug": "20200101-first-adr", + "status": "accepted", + "supersededBy": null, + "tags": Array [ + "foo", + "bar", + ], + "title": "First ADR", +} +`; + +exports[`E2E tests / RO searchAdrs() all 1`] = ` +Array [ + "20200101-first-adr", + "package1/20200101-first-adr", + "20200102-adr-only-with-date", + "20200102-adr-with-intro", + "20200102-adr-without-status", + "20200102-adr-without-title", + "adr_with_a_WeIrd-filename", + "20201028-links", + "package1/20201028-links-in-package", + "package2/20201028-links-to-another-package", + "package1/20201028-links-to-global", + "20201028-superseded-adr", + "20201029-proposed-adr", + "20201029-rejected-adr", + "20201029-superseder", + "20201030-draft-adr", + "20201028-adr-with-no-metadata", + "20201028-adr-with-no-metadata-no-title", + "20201028-adr-without-date", + "package1/20201028-adr-without-date", +] +`; + +exports[`E2E tests / RO searchAdrs() all 2`] = ` +Array [ + Object { + "body": Object { + "enhancedMdx": " +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# First ADR + +- Status: accepted +- Deciders: John Doe, Foo bar +- Date: 2020-01-01 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [ + "John Doe", + "Foo bar", + ], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20200101-first-adr.md", + "relativePath": "docs/adr/20200101-first-adr.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-01-01T22:59:59.000Z", + "slug": "20200101-first-adr", + "status": "accepted", + "supersededBy": null, + "tags": Array [ + "foo", + "bar", + ], + "title": "First ADR", + }, + Object { + "body": Object { + "enhancedMdx": " +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# First ADR (in package 1) + +- Status: accepted +- Deciders: John Doe, Foo bar +- Date: 2020-01-01 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [ + "John Doe", + "Foo bar", + ], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/packages/package1/adr/20200101-first-adr.md", + "relativePath": "packages/package1/adr/20200101-first-adr.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": "package1", + "publicationDate": "2020-01-01T22:59:59.000Z", + "slug": "package1/20200101-first-adr", + "status": "accepted", + "supersededBy": null, + "tags": Array [ + "foo", + "bar", + ], + "title": "First ADR (in package 1)", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# ADR only with date + +- Date: 2020-01-02 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20200102-adr-only-with-date.md", + "relativePath": "docs/adr/20200102-adr-only-with-date.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-01-02T22:59:59.000Z", + "slug": "20200102-adr-only-with-date", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": "ADR only with date", + }, + Object { + "body": Object { + "enhancedMdx": " +This is an introduction paragraph. + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# ADR with intro + +This is an introduction paragraph. + +- Status: accepted +- Deciders: John Doe, Foo bar +- Date: 2020-01-02 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [ + "John Doe", + "Foo bar", + ], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20200102-adr-with-intro.md", + "relativePath": "docs/adr/20200102-adr-with-intro.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-01-02T22:59:59.000Z", + "slug": "20200102-adr-with-intro", + "status": "accepted", + "supersededBy": null, + "tags": Array [ + "foo", + "bar", + ], + "title": "ADR with intro", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# ADR without status + +- Deciders: John Doe +- Date: 2020-01-02 +- Tags: foo + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [ + "John Doe", + ], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20200102-adr-without-status.md", + "relativePath": "docs/adr/20200102-adr-without-status.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-01-02T22:59:59.000Z", + "slug": "20200102-adr-without-status", + "status": "accepted", + "supersededBy": null, + "tags": Array [ + "foo", + ], + "title": "ADR without status", + }, + Object { + "body": Object { + "enhancedMdx": "Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "- Deciders: John Doe, Foo bar +- Date: 2020-01-02 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [ + "John Doe", + "Foo bar", + ], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20200102-adr-without-title.md", + "relativePath": "docs/adr/20200102-adr-without-title.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-01-02T22:59:59.000Z", + "slug": "20200102-adr-without-title", + "status": "accepted", + "supersededBy": null, + "tags": Array [ + "foo", + "bar", + ], + "title": null, + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# ADR with a weird filename + +- Status: accepted +- Date: 2020-01-02 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/adr_with_a_WeIrd-filename.md", + "relativePath": "docs/adr/adr_with_a_WeIrd-filename.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-01-02T22:59:59.000Z", + "slug": "adr_with_a_WeIrd-filename", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": "ADR with a weird filename", + }, + Object { + "body": Object { + "enhancedMdx": " +## Tests + +- Classic link: <AdrLink slug=\\"20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR\\" /> +- Relative link: <AdrLink slug=\\"20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR\\" /> +- <AdrLink slug=\\"20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR\\" customLabel=\\"Link with a custom text\\" /> +- [External link](https://www.google.com/) +- Broken link: [20200101-first-adr](20200101-first-adr-BROKEN.md) +- Link to a package: <AdrLink slug=\\"package1/20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR (in package 1)\\" package=\\"package1\\" /> +", + "rawMarkdown": "# Links + +- Date: 2020-10-28 + +## Tests + +- Classic link: [20200101-first-adr](20200101-first-adr.md) +- Relative link: [20200101-first-adr](./20200101-first-adr.md) +- [Link with a custom text](20200101-first-adr.md) +- [External link](https://www.google.com/) +- Broken link: [20200101-first-adr](20200101-first-adr-BROKEN.md) +- Link to a package: [package1/20200101-first-adr](../../packages/package1/adr/20200101-first-adr.md) +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201028-links.md", + "relativePath": "docs/adr/20201028-links.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-10-28T22:59:59.000Z", + "slug": "20201028-links", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": "Links", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Test link with complete slug: <AdrLink slug=\\"package1/20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR (in package 1)\\" package=\\"package1\\" /> +Test link with partial slug: <AdrLink slug=\\"package1/20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR (in package 1)\\" package=\\"package1\\" /> +<AdrLink slug=\\"package1/20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR (in package 1)\\" package=\\"package1\\" customLabel=\\"Custom text and relative path\\" /> + +## Decision Outcome + +- Relates to <AdrLink slug=\\"package1/20201028-links-to-global\\" status=\\"accepted\\" title=\\"Links to global\\" package=\\"package1\\" /> +", + "rawMarkdown": "# Links in package + +- Date: 2020-10-28 + +## Context and Problem Statement + +Test link with complete slug: [package1/20200101-first-adr](20200101-first-adr.md) +Test link with partial slug: [20200101-first-adr](20200101-first-adr.md) +[Custom text and relative path](./20200101-first-adr.md) + +## Decision Outcome + +- Relates to [package1/20201028-links-to-global](20201028-links-to-global.md) +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/packages/package1/adr/20201028-links-in-package.md", + "relativePath": "packages/package1/adr/20201028-links-in-package.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": "package1", + "publicationDate": "2020-10-28T22:59:59.000Z", + "slug": "package1/20201028-links-in-package", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": "Links in package", + }, + Object { + "body": Object { + "enhancedMdx": " +## Test + +Test link: <AdrLink slug=\\"package1/20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR (in package 1)\\" package=\\"package1\\" /> +<AdrLink slug=\\"package1/20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR (in package 1)\\" package=\\"package1\\" customLabel=\\"Custom text\\" /> +", + "rawMarkdown": "# Links to another package + +- Date: 2020-10-28 + +## Test + +Test link: [package1/20200101-first-adr](../../package1/adr/20200101-first-adr.md) +[Custom text](../../package1/adr/20200101-first-adr.md) +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/packages/package2/adr/20201028-links-to-another-package.md", + "relativePath": "packages/package2/adr/20201028-links-to-another-package.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": "package2", + "publicationDate": "2020-10-28T22:59:59.000Z", + "slug": "package2/20201028-links-to-another-package", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": "Links to another package", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. Test link: <AdrLink slug=\\"20200101-first-adr\\" status=\\"accepted\\" title=\\"First ADR\\" /> + +## Decision Outcome + +- Relates to <AdrLink slug=\\"20201028-links\\" status=\\"accepted\\" title=\\"Links\\" /> +", + "rawMarkdown": "# Links to global + +- Date: 2020-10-28 + +## Context and Problem Statement + +Lorem ipsum. Test link: [20200101-first-adr](../../docs/adr/20200101-first-adr.md) + +## Decision Outcome + +- Relates to [20201028-links](../../docs/adr/20201028-links.md) +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/packages/package1/adr/20201028-links-to-global.md", + "relativePath": "packages/package1/adr/20201028-links-to-global.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": "package1", + "publicationDate": "2020-10-28T22:59:59.000Z", + "slug": "package1/20201028-links-to-global", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": "Links to global", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# Superseded ADR + +- Status: superseded by [20201029-superseder](20201029-superseder.md) +- Date: 2020-10-28 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201028-superseded-adr.md", + "relativePath": "docs/adr/20201028-superseded-adr.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-10-28T22:59:59.000Z", + "slug": "20201028-superseded-adr", + "status": "superseded", + "supersededBy": "20201029-superseder", + "tags": Array [], + "title": "Superseded ADR", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# Proposed ADR + +- Status: proposed +- Date: 2020-10-29 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201029-proposed-adr.md", + "relativePath": "docs/adr/20201029-proposed-adr.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-10-29T22:59:59.000Z", + "slug": "20201029-proposed-adr", + "status": "proposed", + "supersededBy": null, + "tags": Array [], + "title": "Proposed ADR", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# Rejected ADR + +- Status: rejected +- Date: 2020-10-29 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201029-rejected-adr.md", + "relativePath": "docs/adr/20201029-rejected-adr.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-10-29T22:59:59.000Z", + "slug": "20201029-rejected-adr", + "status": "rejected", + "supersededBy": null, + "tags": Array [], + "title": "Rejected ADR", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. + +## Links + +- Supersedes <AdrLink slug=\\"20201028-superseded-adr\\" status=\\"superseded\\" title=\\"Superseded ADR\\" /> +", + "rawMarkdown": "# Superseder + +- Status: accepted +- Date: 2020-10-29 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. + +## Links + +- Supersedes [20201028-superseded-adr](20201028-superseded-adr.md) +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201029-superseder.md", + "relativePath": "docs/adr/20201029-superseder.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-10-29T22:59:59.000Z", + "slug": "20201029-superseder", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": "Superseder", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# Draft ADR + +- Status: draft +- Date: 2020-10-30 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201030-draft-adr.md", + "relativePath": "docs/adr/20201030-draft-adr.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": "2020-10-30T22:59:59.000Z", + "slug": "20201030-draft-adr", + "status": "draft", + "supersededBy": null, + "tags": Array [], + "title": "Draft ADR", + }, + Object { + "body": Object { + "enhancedMdx": " +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# ADR with no metadata + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201028-adr-with-no-metadata.md", + "relativePath": "docs/adr/20201028-adr-with-no-metadata.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": null, + "slug": "20201028-adr-with-no-metadata", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": "ADR with no metadata", + }, + Object { + "body": Object { + "enhancedMdx": "## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201028-adr-with-no-metadata-no-title.md", + "relativePath": "docs/adr/20201028-adr-with-no-metadata-no-title.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": null, + "slug": "20201028-adr-with-no-metadata-no-title", + "status": "accepted", + "supersededBy": null, + "tags": Array [], + "title": null, + }, + Object { + "body": Object { + "enhancedMdx": " +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# ADR without date + +- Status: accepted +- Deciders: John Doe, Foo bar +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [ + "John Doe", + "Foo bar", + ], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/docs/adr/20201028-adr-without-date.md", + "relativePath": "docs/adr/20201028-adr-without-date.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": null, + "publicationDate": null, + "slug": "20201028-adr-without-date", + "status": "accepted", + "supersededBy": null, + "tags": Array [ + "foo", + "bar", + ], + "title": "ADR without date", + }, + Object { + "body": Object { + "enhancedMdx": " +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + "rawMarkdown": "# ADR without date (in package 1) + +- Status: accepted +- Deciders: John Doe, Foo bar +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. +", + }, + "creationDate": "2020-11-19T11:33:33.000Z", + "deciders": Array [ + "John Doe", + "Foo bar", + ], + "file": Object { + "absolutePath": "/ABSOLUTE-PATH/ro-project/packages/package1/adr/20201028-adr-without-date.md", + "relativePath": "packages/package1/adr/20201028-adr-without-date.md", + }, + "lastEditAuthor": "Thomas Vaillant", + "lastEditDate": "2020-11-19T11:33:33.000Z", + "package": "package1", + "publicationDate": null, + "slug": "package1/20201028-adr-without-date", + "status": "accepted", + "supersededBy": null, + "tags": Array [ + "foo", + "bar", + ], + "title": "ADR without date (in package 1)", + }, +] +`; diff --git a/packages/core/integration-tests/__snapshots__/rw.test.ts.snap b/packages/core/integration-tests/__snapshots__/rw.test.ts.snap new file mode 100644 index 00000000..c970eef6 --- /dev/null +++ b/packages/core/integration-tests/__snapshots__/rw.test.ts.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`E2E tests / RW createAdrFromTemplate() in global scope 1`] = ` +" +Technical Story: [description | ticket/issue URL] <!-- optional --> + +## Context and Problem Statement + +[Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] + +<!-- TRUNCATED FOR TESTS --> + +## Decision Outcome + +Chosen option: \\"[option 1]\\", because [justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force force | … | comes out best (see below)]. + +<!-- TRUNCATED FOR TESTS --> + +## Links <!-- optional --> + +- [Link type][link to adr] <!-- example: Refined by [ADR-0005](0005-example.md) --> +- … <!-- numbers of links can vary --> +" +`; + +exports[`E2E tests / RW createAdrFromTemplate() in package with custom template 1`] = ` +" +## Context and Problem Statement + +This is a custom template for this package. +" +`; + +exports[`E2E tests / RW createAdrFromTemplate() in package with global template 1`] = ` +" +Technical Story: [description | ticket/issue URL] <!-- optional --> + +## Context and Problem Statement + +[Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] + +<!-- TRUNCATED FOR TESTS --> + +## Decision Outcome + +Chosen option: \\"[option 1]\\", because [justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force force | … | comes out best (see below)]. + +<!-- TRUNCATED FOR TESTS --> + +## Links <!-- optional --> + +- [Link type][link to adr] <!-- example: Refined by [ADR-0005](0005-example.md) --> +- … <!-- numbers of links can vary --> +" +`; + +exports[`E2E tests / RW supersedeAdr() basic 1`] = ` +" +Technical Story: [description | ticket/issue URL] <!-- optional --> + +## Context and Problem Statement + +[Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] + +<!-- TRUNCATED FOR TESTS --> + +## Decision Outcome + +Chosen option: \\"[option 1]\\", because [justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force force | … | comes out best (see below)]. + +<!-- TRUNCATED FOR TESTS --> + +## Links <!-- optional --> + +- [Link type][link to adr] <!-- example: Refined by [ADR-0005](0005-example.md) --> +- Supersedes <AdrLink slug=\\"superseded\\" status=\\"superseded\\" title=\\"Superseded\\" /> +- … <!-- numbers of links can vary --> +" +`; diff --git a/packages/core/integration-tests/ro-project/.log4brains.yml b/packages/core/integration-tests/ro-project/.log4brains.yml new file mode 100644 index 00000000..7aa7a521 --- /dev/null +++ b/packages/core/integration-tests/ro-project/.log4brains.yml @@ -0,0 +1,13 @@ +--- +project: + name: log4brains-tests-ro + tz: Europe/Paris + adrFolder: ./docs/adr + + packages: + - name: package1 + path: ./packages/package1 + adrFolder: ./packages/package1/adr + - name: package2 + path: ./packages/package2 + adrFolder: ./packages/package2/adr diff --git a/packages/core/integration-tests/ro-project/docs/adr/20200101-first-adr.md b/packages/core/integration-tests/ro-project/docs/adr/20200101-first-adr.md new file mode 100644 index 00000000..d3b906ac --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20200101-first-adr.md @@ -0,0 +1,16 @@ +# First ADR + +- Status: accepted +- Deciders: John Doe, Foo bar +- Date: 2020-01-01 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-only-with-date.md b/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-only-with-date.md new file mode 100644 index 00000000..ae9c3136 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-only-with-date.md @@ -0,0 +1,11 @@ +# ADR only with date + +- Date: 2020-01-02 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-with-intro.md b/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-with-intro.md new file mode 100644 index 00000000..8187a687 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-with-intro.md @@ -0,0 +1,18 @@ +# ADR with intro + +This is an introduction paragraph. + +- Status: accepted +- Deciders: John Doe, Foo bar +- Date: 2020-01-02 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-status.md b/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-status.md new file mode 100644 index 00000000..26ce375e --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-status.md @@ -0,0 +1,13 @@ +# ADR without status + +- Deciders: John Doe +- Date: 2020-01-02 +- Tags: foo + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-title.md b/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-title.md new file mode 100644 index 00000000..858ed5b2 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-title.md @@ -0,0 +1,13 @@ +- Deciders: John Doe, Foo bar +- Date: 2020-01-02 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata-no-title.md b/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata-no-title.md new file mode 100644 index 00000000..a2e91dd8 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata-no-title.md @@ -0,0 +1,7 @@ +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata.md b/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata.md new file mode 100644 index 00000000..41e56bdb --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata.md @@ -0,0 +1,9 @@ +# ADR with no metadata + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-without-date.md b/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-without-date.md new file mode 100644 index 00000000..a65000f2 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201028-adr-without-date.md @@ -0,0 +1,15 @@ +# ADR without date + +- Status: accepted +- Deciders: John Doe, Foo bar +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201028-links.md b/packages/core/integration-tests/ro-project/docs/adr/20201028-links.md new file mode 100644 index 00000000..dd4326cb --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201028-links.md @@ -0,0 +1,12 @@ +# Links + +- Date: 2020-10-28 + +## Tests + +- Classic link: [20200101-first-adr](20200101-first-adr.md) +- Relative link: [20200101-first-adr](./20200101-first-adr.md) +- [Link with a custom text](20200101-first-adr.md) +- [External link](https://www.google.com/) +- Broken link: [20200101-first-adr](20200101-first-adr-BROKEN.md) +- Link to a package: [package1/20200101-first-adr](../../packages/package1/adr/20200101-first-adr.md) diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201028-superseded-adr.md b/packages/core/integration-tests/ro-project/docs/adr/20201028-superseded-adr.md new file mode 100644 index 00000000..f8360d86 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201028-superseded-adr.md @@ -0,0 +1,12 @@ +# Superseded ADR + +- Status: superseded by [20201029-superseder](20201029-superseder.md) +- Date: 2020-10-28 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201029-proposed-adr.md b/packages/core/integration-tests/ro-project/docs/adr/20201029-proposed-adr.md new file mode 100644 index 00000000..5806323e --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201029-proposed-adr.md @@ -0,0 +1,12 @@ +# Proposed ADR + +- Status: proposed +- Date: 2020-10-29 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201029-rejected-adr.md b/packages/core/integration-tests/ro-project/docs/adr/20201029-rejected-adr.md new file mode 100644 index 00000000..ee6729c8 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201029-rejected-adr.md @@ -0,0 +1,12 @@ +# Rejected ADR + +- Status: rejected +- Date: 2020-10-29 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201029-superseder.md b/packages/core/integration-tests/ro-project/docs/adr/20201029-superseder.md new file mode 100644 index 00000000..646d688c --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201029-superseder.md @@ -0,0 +1,16 @@ +# Superseder + +- Status: accepted +- Date: 2020-10-29 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. + +## Links + +- Supersedes [20201028-superseded-adr](20201028-superseded-adr.md) diff --git a/packages/core/integration-tests/ro-project/docs/adr/20201030-draft-adr.md b/packages/core/integration-tests/ro-project/docs/adr/20201030-draft-adr.md new file mode 100644 index 00000000..f0e05700 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/20201030-draft-adr.md @@ -0,0 +1,12 @@ +# Draft ADR + +- Status: draft +- Date: 2020-10-30 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/adr_with_a_WeIrd-filename.md b/packages/core/integration-tests/ro-project/docs/adr/adr_with_a_WeIrd-filename.md new file mode 100644 index 00000000..e2317b94 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/adr_with_a_WeIrd-filename.md @@ -0,0 +1,12 @@ +# ADR with a weird filename + +- Status: accepted +- Date: 2020-01-02 + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/docs/adr/template.md b/packages/core/integration-tests/ro-project/docs/adr/template.md new file mode 100644 index 00000000..56468207 --- /dev/null +++ b/packages/core/integration-tests/ro-project/docs/adr/template.md @@ -0,0 +1,3 @@ +# [short title of solved problem and solution] + +<!-- TRUNCATED FOR TESTS --> diff --git a/packages/core/integration-tests/ro-project/packages/package1/adr/20200101-first-adr.md b/packages/core/integration-tests/ro-project/packages/package1/adr/20200101-first-adr.md new file mode 100644 index 00000000..aec161db --- /dev/null +++ b/packages/core/integration-tests/ro-project/packages/package1/adr/20200101-first-adr.md @@ -0,0 +1,16 @@ +# First ADR (in package 1) + +- Status: accepted +- Deciders: John Doe, Foo bar +- Date: 2020-01-01 +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-adr-without-date.md b/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-adr-without-date.md new file mode 100644 index 00000000..24071253 --- /dev/null +++ b/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-adr-without-date.md @@ -0,0 +1,15 @@ +# ADR without date (in package 1) + +- Status: accepted +- Deciders: John Doe, Foo bar +- Tags: foo, bar + +Technical Story: lorem ipsum + +## Context and Problem Statement + +Lorem ipsum. + +## Decision Outcome + +Lorem ipsum. diff --git a/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-in-package.md b/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-in-package.md new file mode 100644 index 00000000..4919d6a1 --- /dev/null +++ b/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-in-package.md @@ -0,0 +1,13 @@ +# Links in package + +- Date: 2020-10-28 + +## Context and Problem Statement + +Test link with complete slug: [package1/20200101-first-adr](20200101-first-adr.md) +Test link with partial slug: [20200101-first-adr](20200101-first-adr.md) +[Custom text and relative path](./20200101-first-adr.md) + +## Decision Outcome + +- Relates to [package1/20201028-links-to-global](20201028-links-to-global.md) diff --git a/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-to-global.md b/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-to-global.md new file mode 100644 index 00000000..14049df2 --- /dev/null +++ b/packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-to-global.md @@ -0,0 +1,11 @@ +# Links to global + +- Date: 2020-10-28 + +## Context and Problem Statement + +Lorem ipsum. Test link: [20200101-first-adr](../../docs/adr/20200101-first-adr.md) + +## Decision Outcome + +- Relates to [20201028-links](../../docs/adr/20201028-links.md) diff --git a/packages/core/integration-tests/ro-project/packages/package1/adr/template.md b/packages/core/integration-tests/ro-project/packages/package1/adr/template.md new file mode 100644 index 00000000..56468207 --- /dev/null +++ b/packages/core/integration-tests/ro-project/packages/package1/adr/template.md @@ -0,0 +1,3 @@ +# [short title of solved problem and solution] + +<!-- TRUNCATED FOR TESTS --> diff --git a/packages/core/integration-tests/ro-project/packages/package2/adr/20201028-links-to-another-package.md b/packages/core/integration-tests/ro-project/packages/package2/adr/20201028-links-to-another-package.md new file mode 100644 index 00000000..3d65a5a6 --- /dev/null +++ b/packages/core/integration-tests/ro-project/packages/package2/adr/20201028-links-to-another-package.md @@ -0,0 +1,8 @@ +# Links to another package + +- Date: 2020-10-28 + +## Test + +Test link: [package1/20200101-first-adr](../../package1/adr/20200101-first-adr.md) +[Custom text](../../package1/adr/20200101-first-adr.md) diff --git a/packages/core/integration-tests/ro.test.ts b/packages/core/integration-tests/ro.test.ts new file mode 100644 index 00000000..8ed0d579 --- /dev/null +++ b/packages/core/integration-tests/ro.test.ts @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import moment from "moment"; +import path from "path"; +import { Log4brains } from "../src/infrastructure/api/Log4brains"; +import { forceUnixPath } from "../src/lib/paths"; + +function prepareDataForSnapshot(data: any): any { + const json = JSON.stringify(data); + return JSON.parse( + json.replace( + new RegExp(forceUnixPath(path.resolve(__dirname)), "g"), + "/ABSOLUTE-PATH" + ) + ); +} + +describe("E2E tests / RO", () => { + jest.setTimeout(1000 * 15); + + const instance = Log4brains.create(path.join(__dirname, "ro-project")); + + describe("searchAdrs()", () => { + test("all", async () => { + const adrs = await instance.searchAdrs(); + expect(adrs.map((adr) => adr.slug)).toMatchSnapshot(); // To see easily the order + expect(prepareDataForSnapshot(adrs)).toMatchSnapshot(); + }); + + test("with filter on statuses", async () => { + const acceptedAdrs = await instance.searchAdrs({ + statuses: ["accepted"] + }); + expect( + acceptedAdrs.every((adr) => adr.status === "accepted") + ).toBeTruthy(); + + const supersededAdrs = await instance.searchAdrs({ + statuses: ["superseded"] + }); + expect( + supersededAdrs.every((adr) => adr.status === "superseded") + ).toBeTruthy(); + + const acceptedAndSupersededAdrs = await instance.searchAdrs({ + statuses: ["accepted", "superseded"] + }); + expect( + acceptedAndSupersededAdrs.every( + (adr) => adr.status === "accepted" || adr.status === "superseded" + ) + ).toBeTruthy(); + + const acceptedAdrSlugs = acceptedAdrs.map((adr) => adr.slug); + const supersededAdrSlugs = supersededAdrs.map((adr) => adr.slug); + const acceptedAndSupersededAdrSlugs = acceptedAndSupersededAdrs.map( + (adr) => adr.slug + ); + const a = [...acceptedAdrSlugs, ...supersededAdrSlugs].sort(); + const b = [...acceptedAndSupersededAdrSlugs].sort(); + expect(b).toEqual(a); + }); + }); + + describe("getAdrBySlug()", () => { + test("existing ADR", async () => { + const adr = await instance.getAdrBySlug("20200101-first-adr"); + expect(prepareDataForSnapshot(adr)).toMatchSnapshot(); + }); + + test("unknown ADR", async () => { + const adr = await instance.getAdrBySlug("unknown"); + expect(adr).toBeUndefined(); + }); + }); + + describe("generateAdrSlug()", () => { + test("in global scope", async () => { + const date = moment().format("YYYYMMDD"); + expect(await instance.generateAdrSlug("My end-to-end test !")).toEqual( + `${date}-my-end-to-end-test` + ); + }); + + test("in a package", async () => { + const date = moment().format("YYYYMMDD"); + expect( + await instance.generateAdrSlug("My end-to-end test !", "package1") + ).toEqual(`package1/${date}-my-end-to-end-test`); + }); + }); +}); diff --git a/packages/core/integration-tests/rw-project/.gitignore b/packages/core/integration-tests/rw-project/.gitignore new file mode 100644 index 00000000..b03ae66a --- /dev/null +++ b/packages/core/integration-tests/rw-project/.gitignore @@ -0,0 +1,2 @@ +*.md +!template.md diff --git a/packages/core/integration-tests/rw-project/.log4brains.yml b/packages/core/integration-tests/rw-project/.log4brains.yml new file mode 100644 index 00000000..5400b22a --- /dev/null +++ b/packages/core/integration-tests/rw-project/.log4brains.yml @@ -0,0 +1,13 @@ +--- +project: + name: log4brains-tests-rw + tz: Europe/Paris + adrFolder: ./docs/adr + + packages: + - name: package1 + path: ./packages/package1 + adrFolder: ./packages/package1/adr + - name: package2 + path: ./packages/package2 + adrFolder: ./packages/package2/adr diff --git a/packages/core/integration-tests/rw-project/docs/adr/template.md b/packages/core/integration-tests/rw-project/docs/adr/template.md new file mode 100644 index 00000000..8db4c0b8 --- /dev/null +++ b/packages/core/integration-tests/rw-project/docs/adr/template.md @@ -0,0 +1,25 @@ +# [short title of solved problem and solution] + +- Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)] <!-- optional --> +- Deciders: [list everyone involved in the decision] <!-- optional --> +- Date: [YYYY-MM-DD when the decision was last updated] <!-- optional - changes the order displayed in the UI --> +- Tags: [space and/or comma separated list of tags] <!-- optional --> + +Technical Story: [description | ticket/issue URL] <!-- optional --> + +## Context and Problem Statement + +[Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] + +<!-- TRUNCATED FOR TESTS --> + +## Decision Outcome + +Chosen option: "[option 1]", because [justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force force | … | comes out best (see below)]. + +<!-- TRUNCATED FOR TESTS --> + +## Links <!-- optional --> + +- [Link type][link to adr] <!-- example: Refined by [ADR-0005](0005-example.md) --> +- … <!-- numbers of links can vary --> diff --git a/packages/core/integration-tests/rw-project/packages/package1/adr/.gitignore b/packages/core/integration-tests/rw-project/packages/package1/adr/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/packages/core/integration-tests/rw-project/packages/package1/adr/template.md b/packages/core/integration-tests/rw-project/packages/package1/adr/template.md new file mode 100644 index 00000000..daef03e1 --- /dev/null +++ b/packages/core/integration-tests/rw-project/packages/package1/adr/template.md @@ -0,0 +1,9 @@ +# [short title of solved problem and solution] + +- Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)] <!-- optional --> +- Date: [YYYY-MM-DD when the decision was last updated] <!-- optional - changes the order displayed in the UI --> +- Tags: [space and/or comma separated list of tags] <!-- optional --> + +## Context and Problem Statement + +This is a custom template for this package. diff --git a/packages/core/integration-tests/rw-project/packages/package2/adr/.gitignore b/packages/core/integration-tests/rw-project/packages/package2/adr/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/packages/core/integration-tests/rw.test.ts b/packages/core/integration-tests/rw.test.ts new file mode 100644 index 00000000..07d5b0a5 --- /dev/null +++ b/packages/core/integration-tests/rw.test.ts @@ -0,0 +1,112 @@ +import path from "path"; +import globby from "globby"; +import rimraf from "rimraf"; +import moment from "moment"; +import { Log4brains } from "../src/infrastructure/api"; +import { forceUnixPath } from "../src/lib/paths"; + +const PROJECT_PATH = forceUnixPath(path.join(__dirname, "rw-project")); + +function clean(): void { + globby + .sync([`${PROJECT_PATH}/**/*.md`, `!${PROJECT_PATH}/**/template.md`]) + .forEach((fileToClean) => rimraf.sync(fileToClean)); +} + +describe("E2E tests / RW", () => { + jest.setTimeout(1000 * 15); + + beforeAll(clean); + afterAll(clean); + + const instance = Log4brains.create(PROJECT_PATH); + + describe("createAdrFromTemplate()", () => { + test("in global scope", async () => { + await instance.createAdrFromTemplate( + "create-adr-from-template", + "Hello World" + ); + const adr = await instance.getAdrBySlug("create-adr-from-template"); + + expect(adr).toBeDefined(); + expect(adr?.title).toEqual("Hello World"); + expect(adr?.status).toEqual("draft"); + expect(adr?.package).toBeNull(); + expect(adr?.body.enhancedMdx).toMatchSnapshot(); + }); + + test("in package with custom template", async () => { + await instance.createAdrFromTemplate( + "package1/create-adr-from-template-package-custom-template", + "Foo Bar" + ); + const adr = await instance.getAdrBySlug( + "package1/create-adr-from-template-package-custom-template" + ); + + expect(adr).toBeDefined(); + expect(adr?.title).toEqual("Foo Bar"); + expect(adr?.status).toEqual("draft"); + expect(adr?.package).toEqual("package1"); + expect(adr?.body.enhancedMdx).toMatchSnapshot(); + }); + + test("in package with global template", async () => { + await instance.createAdrFromTemplate( + "package2/create-adr-from-template-package-global-template", + "Foo Baz" + ); + const adr = await instance.getAdrBySlug( + "package2/create-adr-from-template-package-global-template" + ); + + expect(adr).toBeDefined(); + expect(adr?.title).toEqual("Foo Baz"); + expect(adr?.status).toEqual("draft"); + expect(adr?.package).toEqual("package2"); + expect(adr?.body.enhancedMdx).toMatchSnapshot(); + }); + + test("slug duplication", async () => { + await instance.createAdrFromTemplate("duplicated-slug", "Hello World"); + await expect( + instance.createAdrFromTemplate("duplicated-slug", "Hello World 2") + ).rejects.toThrow(); + }); + + test("unknown package", async () => { + await expect( + instance.createAdrFromTemplate("unknown-package/test", "Hello World") + ).rejects.toThrow(); + }); + }); + + describe("supersedeAdr()", () => { + test("basic", async () => { + await instance.createAdrFromTemplate("superseded", "Superseded"); + await instance.createAdrFromTemplate("superseder", "Superseder"); + await instance.supersedeAdr("superseded", "superseder"); + + const superseded = await instance.getAdrBySlug("superseded"); + const superseder = await instance.getAdrBySlug("superseder"); + + expect(superseded?.status).toEqual("superseded"); + expect(superseded?.supersededBy).toEqual(superseder?.slug); + expect(superseder?.body.enhancedMdx).toMatchSnapshot(); + }); + }); + + describe("generateAdrSlug()", () => { + test("duplicate", async () => { + const date = moment().format("YYYYMMDD"); + const slug = await instance.generateAdrSlug("Duplicate Test"); + expect(slug).toEqual(`${date}-duplicate-test`); + + await instance.createAdrFromTemplate(slug, "Duplicate Test"); + expect(await instance.generateAdrSlug("Duplicate Test")).toEqual( + `${date}-duplicate-test-2` + ); + }); + }); +}); diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js new file mode 100644 index 00000000..70b54dce --- /dev/null +++ b/packages/core/jest.config.js @@ -0,0 +1,14 @@ +const base = require("../../jest.config.base"); +const { pathsToModuleNameMapper } = require("ts-jest/utils"); +const { compilerOptions } = require("./tsconfig"); +const packageJson = require("./package"); + +module.exports = { + ...base, + name: packageJson.name, + displayName: packageJson.name, + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { + prefix: "<rootDir>/" + }), + setupFiles: ["<rootDir>/src/polyfills.ts"] +}; diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..fdefbf54 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,70 @@ +{ + "name": "@log4brains/core", + "version": "1.0.0-beta.0", + "description": "Log4brains architecture knowledge base core API", + "keywords": [ + "log4brains" + ], + "author": "Thomas Vaillant <thomvaill@bluebricks.dev>", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/thomvaill/log4brains", + "repository": { + "type": "git", + "url": "https://github.com/thomvaill/log4brains", + "directory": "packages/core" + }, + "engines": { + "node": ">=10.23.0" + }, + "files": [ + "dist" + ], + "source": "./src/index.ts", + "main": "./dist/index.js", + "module": "./dist/index.module.js", + "types": "./dist/index.d.ts", + "scripts": { + "dev": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node watch", + "build": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node", + "clean": "rimraf ./dist", + "typescript": "tsc --noEmit", + "test": "jest", + "lint": "eslint . --max-warnings=0", + "typedoc": "typedoc --mode library --includeVersion --readme none --theme minimal --excludePrivate --out docs/typedoc src/index.ts", + "prepublishOnly": "yarn build" + }, + "dependencies": { + "awilix": "^4.2.6", + "cheerio": "^1.0.0-rc.3", + "chokidar": "^3.4.3", + "core-js": "^3.7.0", + "git-url-parse": "^11.4.0", + "joi": "^17.2.1", + "launch-editor": "^2.2.1", + "lodash": "^4.17.20", + "markdown-it": "^11.0.1", + "markdown-it-source-map": "^0.1.1", + "moment": "^2.29.1", + "moment-timezone": "^0.5.32", + "neverthrow": "^2.7.1", + "open": "^7.3.0", + "parse-git-config": "^3.0.0", + "simple-git": "^2.21.0", + "slugify": "^1.4.5", + "yaml": "^1.10.0" + }, + "devDependencies": { + "@types/cheerio": "^0.22.22", + "@types/git-url-parse": "^9.0.0", + "@types/joi": "^14.3.4", + "@types/lodash": "^4.14.161", + "@types/markdown-it": "^10.0.2", + "@types/parse-git-config": "^3.0.0", + "globby": "^11.0.1", + "microbundle": "^0.12.4" + } +} diff --git a/packages/core/src/adr/application/command-handlers/CreateAdrFromTemplateCommandHandler.ts b/packages/core/src/adr/application/command-handlers/CreateAdrFromTemplateCommandHandler.ts new file mode 100644 index 00000000..1a8b3e2c --- /dev/null +++ b/packages/core/src/adr/application/command-handlers/CreateAdrFromTemplateCommandHandler.ts @@ -0,0 +1,31 @@ +import { PackageRef } from "@src/adr/domain"; +import { CommandHandler } from "@src/application"; +import { CreateAdrFromTemplateCommand } from "../commands"; +import { AdrRepository, AdrTemplateRepository } from "../repositories"; + +type Deps = { + adrRepository: AdrRepository; + adrTemplateRepository: AdrTemplateRepository; +}; + +export class CreateAdrFromTemplateCommandHandler implements CommandHandler { + readonly commandClass = CreateAdrFromTemplateCommand; + + private readonly adrRepository: AdrRepository; + + private readonly adrTemplateRepository: AdrTemplateRepository; + + constructor({ adrRepository, adrTemplateRepository }: Deps) { + this.adrRepository = adrRepository; + this.adrTemplateRepository = adrTemplateRepository; + } + + async execute(command: CreateAdrFromTemplateCommand): Promise<void> { + const packageRef = command.slug.packagePart + ? new PackageRef(command.slug.packagePart) + : undefined; + const template = await this.adrTemplateRepository.find(packageRef); + const adr = template.createAdrFromMe(command.slug, command.title); + await this.adrRepository.save(adr); + } +} diff --git a/packages/core/src/adr/application/command-handlers/SupersedeAdrCommandHandler.ts b/packages/core/src/adr/application/command-handlers/SupersedeAdrCommandHandler.ts new file mode 100644 index 00000000..db056cc3 --- /dev/null +++ b/packages/core/src/adr/application/command-handlers/SupersedeAdrCommandHandler.ts @@ -0,0 +1,25 @@ +import { CommandHandler } from "@src/application"; +import { SupersedeAdrCommand } from "../commands"; +import { AdrRepository } from "../repositories"; + +type Deps = { + adrRepository: AdrRepository; +}; + +export class SupersedeAdrCommandHandler implements CommandHandler { + readonly commandClass = SupersedeAdrCommand; + + private readonly adrRepository: AdrRepository; + + constructor({ adrRepository }: Deps) { + this.adrRepository = adrRepository; + } + + async execute(command: SupersedeAdrCommand): Promise<void> { + const supersededAdr = await this.adrRepository.find(command.supersededSlug); + const supersederAdr = await this.adrRepository.find(command.supersederSlug); + supersededAdr.supersedeBy(supersederAdr); + await this.adrRepository.save(supersededAdr); + await this.adrRepository.save(supersederAdr); + } +} diff --git a/packages/core/src/adr/application/command-handlers/index.ts b/packages/core/src/adr/application/command-handlers/index.ts new file mode 100644 index 00000000..ef8c4aa5 --- /dev/null +++ b/packages/core/src/adr/application/command-handlers/index.ts @@ -0,0 +1,2 @@ +export * from "./CreateAdrFromTemplateCommandHandler"; +export * from "./SupersedeAdrCommandHandler"; diff --git a/packages/core/src/adr/application/commands/CreateAdrFromTemplateCommand.ts b/packages/core/src/adr/application/commands/CreateAdrFromTemplateCommand.ts new file mode 100644 index 00000000..fc8857af --- /dev/null +++ b/packages/core/src/adr/application/commands/CreateAdrFromTemplateCommand.ts @@ -0,0 +1,8 @@ +import { AdrSlug } from "@src/adr/domain"; +import { Command } from "@src/application"; + +export class CreateAdrFromTemplateCommand extends Command { + constructor(public readonly slug: AdrSlug, public readonly title: string) { + super(); + } +} diff --git a/packages/core/src/adr/application/commands/SupersedeAdrCommand.ts b/packages/core/src/adr/application/commands/SupersedeAdrCommand.ts new file mode 100644 index 00000000..595cf0fa --- /dev/null +++ b/packages/core/src/adr/application/commands/SupersedeAdrCommand.ts @@ -0,0 +1,11 @@ +import { AdrSlug } from "@src/adr/domain"; +import { Command } from "@src/application"; + +export class SupersedeAdrCommand extends Command { + constructor( + public readonly supersededSlug: AdrSlug, + public readonly supersederSlug: AdrSlug + ) { + super(); + } +} diff --git a/packages/core/src/adr/application/commands/index.ts b/packages/core/src/adr/application/commands/index.ts new file mode 100644 index 00000000..7508c3b7 --- /dev/null +++ b/packages/core/src/adr/application/commands/index.ts @@ -0,0 +1,2 @@ +export * from "./CreateAdrFromTemplateCommand"; +export * from "./SupersedeAdrCommand"; diff --git a/packages/core/src/adr/application/index.ts b/packages/core/src/adr/application/index.ts new file mode 100644 index 00000000..bc4a4f36 --- /dev/null +++ b/packages/core/src/adr/application/index.ts @@ -0,0 +1,5 @@ +export * from "./command-handlers"; +export * from "./commands"; +export * from "./queries"; +export * from "./query-handlers"; +export * from "./repositories"; diff --git a/packages/core/src/adr/application/queries/GenerateAdrSlugFromTitleCommand.ts b/packages/core/src/adr/application/queries/GenerateAdrSlugFromTitleCommand.ts new file mode 100644 index 00000000..1092e1ae --- /dev/null +++ b/packages/core/src/adr/application/queries/GenerateAdrSlugFromTitleCommand.ts @@ -0,0 +1,11 @@ +import { PackageRef } from "@src/adr/domain"; +import { Query } from "@src/application"; + +export class GenerateAdrSlugFromTitleQuery extends Query { + constructor( + public readonly title: string, + public readonly packageRef?: PackageRef + ) { + super(); + } +} diff --git a/packages/core/src/adr/application/queries/GetAdrBySlugQuery.ts b/packages/core/src/adr/application/queries/GetAdrBySlugQuery.ts new file mode 100644 index 00000000..0952ec28 --- /dev/null +++ b/packages/core/src/adr/application/queries/GetAdrBySlugQuery.ts @@ -0,0 +1,8 @@ +import { AdrSlug } from "@src/adr/domain"; +import { Query } from "@src/application"; + +export class GetAdrBySlugQuery extends Query { + constructor(public readonly slug: AdrSlug) { + super(); + } +} diff --git a/packages/core/src/adr/application/queries/SearchAdrsQuery.ts b/packages/core/src/adr/application/queries/SearchAdrsQuery.ts new file mode 100644 index 00000000..272a2ae0 --- /dev/null +++ b/packages/core/src/adr/application/queries/SearchAdrsQuery.ts @@ -0,0 +1,12 @@ +import { AdrStatus } from "@src/adr/domain"; +import { Query } from "@src/application"; + +export type SearchAdrsFilters = { + statuses?: AdrStatus[]; +}; + +export class SearchAdrsQuery extends Query { + constructor(public readonly filters: SearchAdrsFilters) { + super(); + } +} diff --git a/packages/core/src/adr/application/queries/index.ts b/packages/core/src/adr/application/queries/index.ts new file mode 100644 index 00000000..83bd940d --- /dev/null +++ b/packages/core/src/adr/application/queries/index.ts @@ -0,0 +1,3 @@ +export * from "./GenerateAdrSlugFromTitleCommand"; +export * from "./GetAdrBySlugQuery"; +export * from "./SearchAdrsQuery"; diff --git a/packages/core/src/adr/application/query-handlers/GenerateAdrSlugFromTitleCommandHandler.ts b/packages/core/src/adr/application/query-handlers/GenerateAdrSlugFromTitleCommandHandler.ts new file mode 100644 index 00000000..0229ab1a --- /dev/null +++ b/packages/core/src/adr/application/query-handlers/GenerateAdrSlugFromTitleCommandHandler.ts @@ -0,0 +1,24 @@ +import { AdrSlug } from "@src/adr/domain"; +import { QueryHandler } from "@src/application"; +import { GenerateAdrSlugFromTitleQuery } from "../queries"; +import { AdrRepository } from "../repositories"; + +type Deps = { + adrRepository: AdrRepository; +}; + +export class GenerateAdrSlugFromTitleQueryHandler implements QueryHandler { + readonly queryClass = GenerateAdrSlugFromTitleQuery; + + private readonly adrRepository: AdrRepository; + + constructor({ adrRepository }: Deps) { + this.adrRepository = adrRepository; + } + + execute(query: GenerateAdrSlugFromTitleQuery): Promise<AdrSlug> { + return Promise.resolve( + this.adrRepository.generateAvailableSlug(query.title, query.packageRef) + ); + } +} diff --git a/packages/core/src/adr/application/query-handlers/GetAdrBySlugQueryHandler.ts b/packages/core/src/adr/application/query-handlers/GetAdrBySlugQueryHandler.ts new file mode 100644 index 00000000..bdddb66d --- /dev/null +++ b/packages/core/src/adr/application/query-handlers/GetAdrBySlugQueryHandler.ts @@ -0,0 +1,32 @@ +import { Adr } from "@src/adr/domain"; +import { QueryHandler } from "@src/application"; +import { Log4brainsError } from "@src/domain"; +import { GetAdrBySlugQuery } from "../queries"; +import { AdrRepository } from "../repositories"; + +type Deps = { + adrRepository: AdrRepository; +}; + +export class GetAdrBySlugQueryHandler implements QueryHandler { + readonly queryClass = GetAdrBySlugQuery; + + private readonly adrRepository: AdrRepository; + + constructor({ adrRepository }: Deps) { + this.adrRepository = adrRepository; + } + + async execute(query: GetAdrBySlugQuery): Promise<Adr | undefined> { + try { + return await this.adrRepository.find(query.slug); + } catch (e) { + if ( + !(e instanceof Log4brainsError && e.name === "This ADR does not exist") + ) { + throw e; + } + } + return undefined; + } +} diff --git a/packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.test.ts b/packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.test.ts new file mode 100644 index 00000000..a260ffe9 --- /dev/null +++ b/packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.test.ts @@ -0,0 +1,53 @@ +import { mock, mockClear } from "jest-mock-extended"; +import { AdrRepository } from "@src/adr/application"; +import { + Adr, + AdrFile, + AdrSlug, + AdrStatus, + FilesystemPath, + MarkdownBody +} from "@src/adr/domain"; +import { SearchAdrsQuery } from "../queries"; +import { SearchAdrsQueryHandler } from "./SearchAdrsQueryHandler"; + +describe("SearchAdrsQueryHandler", () => { + const adr1 = new Adr({ + slug: new AdrSlug("adr1"), + file: new AdrFile(new FilesystemPath("/", "adr1.md")), + body: new MarkdownBody("") + }); + const adr2 = new Adr({ + slug: new AdrSlug("adr2"), + file: new AdrFile(new FilesystemPath("/", "adr2.md")), + body: new MarkdownBody("") + }); + + const adrRepository = mock<AdrRepository>(); + adrRepository.findAll.mockReturnValue(Promise.resolve([adr1, adr2])); + + const handler = new SearchAdrsQueryHandler({ adrRepository }); + + beforeAll(() => { + Adr.setTz("Etc/UTC"); + }); + afterAll(() => { + Adr.clearTz(); + }); + + beforeEach(() => { + mockClear(adrRepository); + }); + + it("returns all ADRs when no filter", async () => { + const adrs = await handler.execute(new SearchAdrsQuery({})); + expect(adrs).toHaveLength(2); + }); + + it("filters the ADRs on their status", async () => { + const adrs = await handler.execute( + new SearchAdrsQuery({ statuses: [AdrStatus.createFromName("proposed")] }) + ); + expect(adrs).toHaveLength(0); + }); +}); diff --git a/packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.ts b/packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.ts new file mode 100644 index 00000000..41ab60c0 --- /dev/null +++ b/packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.ts @@ -0,0 +1,31 @@ +import { Adr } from "@src/adr/domain"; +import { QueryHandler } from "@src/application"; +import { ValueObjectArray } from "@src/domain"; +import { SearchAdrsQuery } from "../queries"; +import { AdrRepository } from "../repositories"; + +type Deps = { + adrRepository: AdrRepository; +}; + +export class SearchAdrsQueryHandler implements QueryHandler { + readonly queryClass = SearchAdrsQuery; + + private readonly adrRepository: AdrRepository; + + constructor({ adrRepository }: Deps) { + this.adrRepository = adrRepository; + } + + async execute(query: SearchAdrsQuery): Promise<Adr[]> { + return (await this.adrRepository.findAll()).filter((adr) => { + if ( + query.filters.statuses && + !ValueObjectArray.inArray(adr.status, query.filters.statuses) + ) { + return false; + } + return true; + }); + } +} diff --git a/packages/core/src/adr/application/query-handlers/index.ts b/packages/core/src/adr/application/query-handlers/index.ts new file mode 100644 index 00000000..53bda2ed --- /dev/null +++ b/packages/core/src/adr/application/query-handlers/index.ts @@ -0,0 +1,3 @@ +export * from "./GenerateAdrSlugFromTitleCommandHandler"; +export * from "./GetAdrBySlugQueryHandler"; +export * from "./SearchAdrsQueryHandler"; diff --git a/packages/core/src/adr/application/repositories/AdrRepository.ts b/packages/core/src/adr/application/repositories/AdrRepository.ts new file mode 100644 index 00000000..8b3aed36 --- /dev/null +++ b/packages/core/src/adr/application/repositories/AdrRepository.ts @@ -0,0 +1,8 @@ +import { Adr, AdrSlug, PackageRef } from "@src/adr/domain"; + +export interface AdrRepository { + find(slug: AdrSlug): Promise<Adr>; + findAll(): Promise<Adr[]>; + generateAvailableSlug(title: string, packageRef?: PackageRef): AdrSlug; + save(adr: Adr): Promise<void>; +} diff --git a/packages/core/src/adr/application/repositories/AdrTemplateRepository.ts b/packages/core/src/adr/application/repositories/AdrTemplateRepository.ts new file mode 100644 index 00000000..b3b25b8b --- /dev/null +++ b/packages/core/src/adr/application/repositories/AdrTemplateRepository.ts @@ -0,0 +1,5 @@ +import { AdrTemplate, PackageRef } from "@src/adr/domain"; + +export interface AdrTemplateRepository { + find(packageRef?: PackageRef): Promise<AdrTemplate>; +} diff --git a/packages/core/src/adr/application/repositories/index.ts b/packages/core/src/adr/application/repositories/index.ts new file mode 100644 index 00000000..95772044 --- /dev/null +++ b/packages/core/src/adr/application/repositories/index.ts @@ -0,0 +1,2 @@ +export * from "./AdrRepository"; +export * from "./AdrTemplateRepository"; diff --git a/packages/core/src/adr/domain/Adr.test.ts b/packages/core/src/adr/domain/Adr.test.ts new file mode 100644 index 00000000..1a19465d --- /dev/null +++ b/packages/core/src/adr/domain/Adr.test.ts @@ -0,0 +1,440 @@ +import moment from "moment-timezone"; +import { Adr } from "./Adr"; +import { AdrSlug } from "./AdrSlug"; +import { AdrStatus } from "./AdrStatus"; +import { MarkdownAdrLink } from "./MarkdownAdrLink"; +import { MarkdownBody } from "./MarkdownBody"; + +describe("Adr", () => { + beforeAll(() => { + Adr.setTz("Etc/UTC"); + }); + afterAll(() => { + Adr.clearTz(); + }); + + describe("get title()", () => { + it("returns the title", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# Lorem Ipsum`) + }); + expect(adr.title).toEqual("Lorem Ipsum"); + }); + + it("returns undefined when no title", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`## Subtitle`) + }); + expect(adr.title).toBeUndefined(); + }); + }); + + describe("get status()", () => { + it("returns the status", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + + - Status: accepted + `) + }); + expect(adr.status.equals(AdrStatus.ACCEPTED)).toBeTruthy(); + }); + + it("returns the SUPERSEDED special status", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + + - Status: superseded by XXX + `) + }); + expect(adr.status.equals(AdrStatus.SUPERSEDED)).toBeTruthy(); + }); + + it("returns the default status", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR`) + }); + expect(adr.status.equals(AdrStatus.ACCEPTED)).toBeTruthy(); + }); + }); + + describe("get superseder()", () => { + it("returns undefined when no relevant", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + + - Status: accepted + `) + }); + expect(adr.superseder).toBeUndefined(); + }); + + it("returns the superseder", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + + - Status: superseded by foo/bar + `) + }); + expect(adr.superseder?.value).toEqual("foo/bar"); + }); + }); + + describe("get tags()", () => { + it("returns the tags", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + + - Tags: frontend,BaCkEnD with-space, with-space-and-comma, with-a-lot-of-spaces + `) + }); + expect(adr.tags).toEqual([ + "frontend", + "backend", + "with-space", + "with-space-and-comma", + "with-a-lot-of-spaces" + ]); + }); + + it("returns no tags", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + + - Status: accepted + `) + }); + expect(adr.tags).toEqual([]); + }); + }); + + describe("get deciders()", () => { + it("returns the deciders", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + + - Deciders: John Doe,Lorem Ipsum test , FOO BAR,bar + `) + }); + expect(adr.deciders).toEqual([ + "John Doe", + "Lorem Ipsum test", + "FOO BAR", + "bar" + ]); + }); + + it("returns no deciders", () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + + - Status: accepted + `) + }); + expect(adr.deciders).toEqual([]); + }); + }); + + describe("getEnhancedMdx()", () => { + const markdownAdrLinkResolver = { + resolve: ( + from: Adr, + uri: string + ): Promise<MarkdownAdrLink | undefined> => { + if (uri === "test-link.md") { + return Promise.resolve( + new MarkdownAdrLink( + from, + new Adr({ + slug: new AdrSlug("test-link"), + body: new MarkdownBody("") + }) + ) + ); + } + return Promise.resolve(undefined); + } + }; + + test("default case", async () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + +- Status: accepted +- Deciders: John Doe, Lorem Ipsum +- Date: 2020-01-01 +- Tags: foo bar + +## Subtitle + +Hello +`).setAdrLinkResolver(markdownAdrLinkResolver) + }); + + expect(await adr.getEnhancedMdx()).toEqual(` +## Subtitle + +Hello +`); + }); + + test("with additional information", async () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# My ADR + +Hello this is a paragraph. + +- Status: accepted +- Deciders: John Doe, Lorem Ipsum +- Date: 2020-01-01 +- Unknown Metadata: test +- Tags: foo bar +- Unknown Metadata2: test2 + +Technical Story: test + +## Subtitle + +Hello +`).setAdrLinkResolver(markdownAdrLinkResolver) + }); + + expect(await adr.getEnhancedMdx()).toEqual(` +Hello this is a paragraph. + +- Unknown Metadata: test +- Unknown Metadata2: test2 + +Technical Story: test + +## Subtitle + +Hello +`); + }); + + test("links replacement", async () => { + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`## Subtitle + +Link to an actual ADR: [custom text](test-link.md). +Link to an actual ADR: [test-link](test-link.md). +Link to an unknown ADR: [lorem ipsum](unknown.md). +Link to an other file: [lorem ipsum](test.html). +Link to an URL: [lorem ipsum](https://www.google.com/). +`).setAdrLinkResolver(markdownAdrLinkResolver) + }); + + expect(await adr.getEnhancedMdx()).toEqual(`## Subtitle + +Link to an actual ADR: <AdrLink slug="test-link" status="accepted" customLabel="custom text" />. +Link to an actual ADR: <AdrLink slug="test-link" status="accepted" />. +Link to an unknown ADR: [lorem ipsum](unknown.md). +Link to an other file: [lorem ipsum](test.html). +Link to an URL: [lorem ipsum](https://www.google.com/). +`); + }); + }); + + describe("compare()", () => { + function bodyWithDate(date: string): MarkdownBody { + return new MarkdownBody(`# Test\n\n- Date: ${date}\n`); + } + function bodyWithoutDate(): MarkdownBody { + return new MarkdownBody("# Test\n"); + } + + describe("when there is a publicationDate", () => { + it("sorts between two publicationDates", () => { + const one = new Adr({ + slug: new AdrSlug("bbb"), + creationDate: new Date("2020-01-02 00:00:00"), + body: bodyWithDate("2020-01-01") + }); + const two = new Adr({ + slug: new AdrSlug("aaa"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithDate("2020-01-02") + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + + it("sorts between a publicationDate and a creationDate", () => { + const one = new Adr({ + slug: new AdrSlug("bbb"), + creationDate: new Date("2020-01-02 00:00:00"), + body: bodyWithoutDate() + }); + const two = new Adr({ + slug: new AdrSlug("aaa"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithDate("2020-01-03") + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + + test("a publicationDate on the same day of a creationDate is always older", () => { + const one = new Adr({ + slug: new AdrSlug("bbb"), + creationDate: new Date("2020-01-02 00:00:00"), + body: bodyWithoutDate() + }); + const two = new Adr({ + slug: new AdrSlug("aaa"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithDate("2020-01-02") + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + + it("sorts by slug when two same publicationDates", () => { + const one = new Adr({ + slug: new AdrSlug("aaa"), + creationDate: new Date("2020-01-02 00:00:00"), + body: bodyWithDate("2020-01-02") + }); + const two = new Adr({ + slug: new AdrSlug("bbb"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithDate("2020-01-02") + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + }); + + describe("when there is no publicationDate", () => { + it("sorts between two creationDate", () => { + const one = new Adr({ + slug: new AdrSlug("bbb"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + const two = new Adr({ + slug: new AdrSlug("aaa"), + creationDate: new Date("2020-01-02 00:00:00"), + body: bodyWithoutDate() + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + + it("sorts by slug when two same creationDates", () => { + const one = new Adr({ + slug: new AdrSlug("aaa"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + const two = new Adr({ + slug: new AdrSlug("abb"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + + it("does not take slug's package part into account", () => { + const one = new Adr({ + slug: new AdrSlug("zzz/aaa"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + const two = new Adr({ + slug: new AdrSlug("bbb"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + + it("does not take case into account", () => { + const one = new Adr({ + slug: new AdrSlug("AAA"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + const two = new Adr({ + slug: new AdrSlug("bbb"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + + it("sorts numbers before letters", () => { + const one = new Adr({ + slug: new AdrSlug("999"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + const two = new Adr({ + slug: new AdrSlug("aaa"), + creationDate: new Date("2020-01-01 00:00:00"), + body: bodyWithoutDate() + }); + + expect([two, one].sort(Adr.compare)).toEqual([one, two]); + expect([one, two].sort(Adr.compare)).toEqual([one, two]); + }); + }); + }); +}); + +describe("Adr - timezones", () => { + it("fails when not calling setTz() before getting publicationDate", () => { + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# Lorem Ipsum`) + }).publicationDate; + }).toThrow(); + }); + + test("timezone works", () => { + Adr.setTz("Europe/Paris"); + + const expectedDate = moment.tz("2020-01-01 23:59:59", "Europe/Paris"); + const adr = new Adr({ + slug: new AdrSlug("test"), + body: new MarkdownBody(`# Lorem Ipsum + + - Date: ${expectedDate.format("YYYY-MM-DD")} +`) + }); + + expect(adr.publicationDate?.toJSON()).toEqual( + expectedDate.toDate().toJSON() + ); + + Adr.clearTz(); + }); +}); diff --git a/packages/core/src/adr/domain/Adr.ts b/packages/core/src/adr/domain/Adr.ts new file mode 100644 index 00000000..4963d127 --- /dev/null +++ b/packages/core/src/adr/domain/Adr.ts @@ -0,0 +1,231 @@ +import moment from "moment-timezone"; +import { AggregateRoot, Log4brainsError } from "@src/domain"; +import { AdrFile } from "./AdrFile"; +import { AdrSlug } from "./AdrSlug"; +import { AdrStatus } from "./AdrStatus"; +import type { MarkdownBody } from "./MarkdownBody"; +import { PackageRef } from "./PackageRef"; +import { AdrRelation } from "./AdrRelation"; +import { Author } from "./Author"; + +// TODO: make this configurable +const dateFormats = ["YYYY-MM-DD", "DD/MM/YYYY"]; + +type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; + +type Props = { + slug: AdrSlug; + package?: PackageRef; + body: MarkdownBody; + file?: AdrFile; // set by the repository after save() + creationDate: Date; // set by the repository after save() or automatically set to now() + lastEditDate: Date; // set by the repository after save() or automatically set to now() + lastEditAuthor: Author; // set by the repository after save() or automatically set to anonymous +}; + +export class Adr extends AggregateRoot<Props> { + /** + * Global TimeZone. + * This static property must be set at startup with Adr.setTz(), otherwise it will throw an Error. + * This dirty behavior is temporary, until we get a better vision on how to deal with timezones in the project. + * TODO: refactor + */ + private static tz?: string; + + constructor( + props: WithOptional< + Props, + "creationDate" | "lastEditDate" | "lastEditAuthor" + > + ) { + super({ + creationDate: props.creationDate || new Date(), + lastEditDate: props.lastEditDate || new Date(), + lastEditAuthor: props.lastEditAuthor || Author.createAnonymous(), + ...props + }); + } + + /** + * @see Adr.tz + */ + static setTz(tz: string): void { + if (!moment.tz.zone(tz)) { + throw new Log4brainsError("Unknown timezone", Adr.tz); + } + Adr.tz = tz; + } + + /** + * For test purposes only + */ + static clearTz(): void { + Adr.tz = undefined; + } + + get slug(): AdrSlug { + return this.props.slug; + } + + get package(): PackageRef | undefined { + return this.props.package; + } + + get body(): MarkdownBody { + return this.props.body; + } + + get file(): AdrFile | undefined { + return this.props.file; + } + + get creationDate(): Date { + return this.props.creationDate; + } + + get lastEditDate(): Date { + return this.props.lastEditDate; + } + + get lastEditAuthor(): Author { + return this.props.lastEditAuthor; + } + + get title(): string | undefined { + return this.body.getFirstH1Title(); // TODO: log when no title + } + + get status(): AdrStatus { + const statusStr = this.body.getHeaderMetadata("Status"); + if (!statusStr) { + return AdrStatus.ACCEPTED; + } + try { + return AdrStatus.createFromName(statusStr); + } catch (e) { + return AdrStatus.DRAFT; // TODO: log (DRAFT because usually the help from the template) + } + } + + get superseder(): AdrSlug | undefined { + const statusStr = this.body.getHeaderMetadata("Status"); + if (!this.status.equals(AdrStatus.SUPERSEDED) || !statusStr) { + return undefined; + } + const slug = statusStr.replace(/superseded\s*by\s*:?/i, "").trim(); + try { + return slug ? new AdrSlug(slug) : undefined; + } catch (e) { + return undefined; // TODO: log + } + } + + get publicationDate(): Date | undefined { + if (!Adr.tz) { + throw new Log4brainsError("Adr.setTz() must be called at startup!"); + } + + const dateStr = this.body.getHeaderMetadata("date"); + if (!dateStr) { + return undefined; + } + + // We set hours on 23:59:59 local time for sorting reasons: + // Because an ADR without a publication date is sorted based on its creationDate. + // And usually, ADRs created on the same publicationDate of another ADR are older than this one. + // This enables us to have a consistent behavior in sorting. + const date = moment.tz( + `${dateStr} 23:59:59`, + dateFormats.map((format) => `${format} HH:mm:ss`), + true, + Adr.tz + ); + if (!date.isValid()) { + return undefined; // TODO: warning + } + return date.toDate(); + } + + get tags(): string[] { + const tags = this.body.getHeaderMetadata("tags"); + if ( + !tags || + tags.trim() === "" || + tags === "[space and/or comma separated list of tags] <!-- optional -->" + ) { + return []; + } + return tags.split(/\s*[\s,]{1}\s*/).map((tag) => tag.trim().toLowerCase()); + } + + get deciders(): string[] { + const deciders = this.body.getHeaderMetadata("deciders"); + if ( + !deciders || + deciders.trim() === "" || + deciders === "[list everyone involved in the decision] <!-- optional -->" + ) { + return []; + } + return deciders.split(/\s*[,]{1}\s*/).map((decider) => decider.trim()); + } + + setFile(file: AdrFile): void { + this.props.file = file; + } + + setTitle(title: string): void { + this.body.setFirstH1Title(title); + } + + supersedeBy(superseder: Adr): void { + const relation = new AdrRelation(this, "superseded by", superseder); + this.body.setHeaderMetadata("Status", relation.toMarkdown()); + superseder.markAsSuperseder(this); + } + + private markAsSuperseder(superseded: Adr): void { + const relation = new AdrRelation(this, "Supersedes", superseded); + this.body.addLinkNoDuplicate(relation.toMarkdown()); + } + + async getEnhancedMdx(): Promise<string> { + const bodyCopy = this.body.clone(); + + // Remove title + bodyCopy.deleteFirstH1Title(); + + // Remove header metadata + ["status", "deciders", "date", "tags"].forEach((metadata) => + bodyCopy.deleteHeaderMetadata(metadata) + ); + + // Replace links + await bodyCopy.replaceAdrLinks(this); + + return bodyCopy.getRawMarkdown(); + } + + static compare(a: Adr, b: Adr): number { + // PublicationDate always wins on creationDate + const aDate = a.publicationDate || a.creationDate; + const bDate = b.publicationDate || b.creationDate; + + const dateDiff = aDate.getTime() - bDate.getTime(); + if (dateDiff !== 0) { + return dateDiff; + } + + // When the dates are equal, we compare the slugs' name parts + const aSlugNamePart = a.slug.namePart.toLowerCase(); + const bSlugNamePart = b.slug.namePart.toLowerCase(); + + if (aSlugNamePart === bSlugNamePart) { + // Special case: when the name parts are equal, we take the package name into account + // This case is very rare but we have to take it into account so that the results are not random + return a.slug.value.toLowerCase() < b.slug.value.toLowerCase() ? -1 : 1; + } + + return aSlugNamePart < bSlugNamePart ? -1 : 1; + } +} diff --git a/packages/core/src/adr/domain/AdrFile.test.ts b/packages/core/src/adr/domain/AdrFile.test.ts new file mode 100644 index 00000000..7265cd94 --- /dev/null +++ b/packages/core/src/adr/domain/AdrFile.test.ts @@ -0,0 +1,35 @@ +import { AdrFile } from "./AdrFile"; +import { AdrSlug } from "./AdrSlug"; +import { FilesystemPath } from "./FilesystemPath"; + +describe("AdrFile", () => { + it("throws when not .md", () => { + expect(() => { + new AdrFile(new FilesystemPath("/", "test")); + }).toThrow(); + }); + + it("throws when reserved filename", () => { + expect(() => { + new AdrFile(new FilesystemPath("/", "template.md")); + }).toThrow(); + expect(() => { + new AdrFile(new FilesystemPath("/", "README.md")); + }).toThrow(); + expect(() => { + new AdrFile(new FilesystemPath("/", "index.md")); + }).toThrow(); + expect(() => { + new AdrFile(new FilesystemPath("/", "backlog.md")); + }).toThrow(); + }); + + it("creates from slug in folder", () => { + expect( + AdrFile.createFromSlugInFolder( + new FilesystemPath("/", "test"), + new AdrSlug("my-package/20200101-hello-world") + ).path.absolutePath + ).toEqual("/test/20200101-hello-world.md"); + }); +}); diff --git a/packages/core/src/adr/domain/AdrFile.ts b/packages/core/src/adr/domain/AdrFile.ts new file mode 100644 index 00000000..592305c2 --- /dev/null +++ b/packages/core/src/adr/domain/AdrFile.ts @@ -0,0 +1,52 @@ +import { Log4brainsError, ValueObject } from "@src/domain"; +import type { AdrSlug } from "./AdrSlug"; +import { FilesystemPath } from "./FilesystemPath"; + +type Props = { + path: FilesystemPath; +}; + +const reservedFilenames = [ + "template.md", + "readme.md", + "index.md", + "backlog.md" +]; + +export class AdrFile extends ValueObject<Props> { + constructor(path: FilesystemPath) { + super({ path }); + + if (path.extension.toLowerCase() !== ".md") { + throw new Log4brainsError( + "Only .md files are supported", + path.pathRelativeToCwd + ); + } + + if (reservedFilenames.includes(path.basename.toLowerCase())) { + throw new Log4brainsError("Reserved ADR filename", path.basename); + } + } + + get path(): FilesystemPath { + return this.props.path; + } + + static isPathValid(path: FilesystemPath): boolean { + try { + // eslint-disable-next-line no-new + new AdrFile(path); + return true; + } catch (e) { + return false; + } + } + + static createFromSlugInFolder( + folder: FilesystemPath, + slug: AdrSlug + ): AdrFile { + return new AdrFile(folder.join(`${slug.namePart}.md`)); + } +} diff --git a/packages/core/src/adr/domain/AdrRelation.test.ts b/packages/core/src/adr/domain/AdrRelation.test.ts new file mode 100644 index 00000000..5853a858 --- /dev/null +++ b/packages/core/src/adr/domain/AdrRelation.test.ts @@ -0,0 +1,42 @@ +import { Adr } from "./Adr"; +import { AdrFile } from "./AdrFile"; +import { AdrRelation } from "./AdrRelation"; +import { AdrSlug } from "./AdrSlug"; +import { FilesystemPath } from "./FilesystemPath"; +import { MarkdownBody } from "./MarkdownBody"; +import { PackageRef } from "./PackageRef"; + +describe("AdrRelation", () => { + beforeAll(() => { + Adr.setTz("Etc/UTC"); + }); + afterAll(() => { + Adr.clearTz(); + }); + + it("correctly prints to markdown", () => { + const from = new Adr({ + slug: new AdrSlug("from"), + file: new AdrFile(new FilesystemPath("/", "docs/adr/from.md")), + body: new MarkdownBody("") + }); + const to = new Adr({ + slug: new AdrSlug("test/to"), + package: new PackageRef("test"), + file: new AdrFile( + new FilesystemPath("/", "packages/test/docs/adr/to.md") + ), + body: new MarkdownBody("") + }); + + const relation1 = new AdrRelation(from, "superseded by", to); + expect(relation1.toMarkdown()).toEqual( + "superseded by [test/to](../../packages/test/docs/adr/to.md)" + ); + + const relation2 = new AdrRelation(from, "refines", to); + expect(relation2.toMarkdown()).toEqual( + "refines [test/to](../../packages/test/docs/adr/to.md)" + ); + }); +}); diff --git a/packages/core/src/adr/domain/AdrRelation.ts b/packages/core/src/adr/domain/AdrRelation.ts new file mode 100644 index 00000000..720c182f --- /dev/null +++ b/packages/core/src/adr/domain/AdrRelation.ts @@ -0,0 +1,32 @@ +import { ValueObject } from "@src/domain"; +import type { Adr } from "./Adr"; +import { MarkdownAdrLink } from "./MarkdownAdrLink"; + +type Props = { + from: Adr; + relation: string; + to: Adr; +}; + +export class AdrRelation extends ValueObject<Props> { + constructor(from: Adr, relation: string, to: Adr) { + super({ from, relation, to }); + } + + get from(): Adr { + return this.props.from; + } + + get relation(): string { + return this.props.relation; + } + + get to(): Adr { + return this.props.to; + } + + toMarkdown(): string { + const link = new MarkdownAdrLink(this.from, this.to); + return `${this.relation} ${link.toMarkdown()}`; + } +} diff --git a/packages/core/src/adr/domain/AdrSlug.test.ts b/packages/core/src/adr/domain/AdrSlug.test.ts new file mode 100644 index 00000000..00317030 --- /dev/null +++ b/packages/core/src/adr/domain/AdrSlug.test.ts @@ -0,0 +1,65 @@ +import { AdrFile } from "./AdrFile"; +import { AdrSlug } from "./AdrSlug"; +import { FilesystemPath } from "./FilesystemPath"; +import { PackageRef } from "./PackageRef"; + +describe("AdrSlug", () => { + it("returns the package part", () => { + expect(new AdrSlug("my-package/0001-test").packagePart).toEqual( + "my-package" + ); + expect(new AdrSlug("0001-test").packagePart).toBeUndefined(); + }); + + it("returns the name part", () => { + expect(new AdrSlug("my-package/0001-test").namePart).toEqual("0001-test"); + expect(new AdrSlug("0001-test").namePart).toEqual("0001-test"); + }); + + describe("createFromFile()", () => { + it("creates from AdrFile with package", () => { + expect( + AdrSlug.createFromFile( + new AdrFile(new FilesystemPath("/", "0001-my-adr.md")), + new PackageRef("my-package") + ).value + ).toEqual("my-package/0001-my-adr"); + }); + + it("creates from AdrFile without package", () => { + expect( + AdrSlug.createFromFile( + new AdrFile(new FilesystemPath("/", "0001-my-adr.md")) + ).value + ).toEqual("0001-my-adr"); + }); + }); + + describe("createFromTitle()", () => { + it("creates from title with package", () => { + expect( + AdrSlug.createFromTitle( + "My ADR", + new PackageRef("my-package"), + new Date(2020, 0, 1) + ).value + ).toEqual("my-package/20200101-my-adr"); + }); + + it("creates from title without package", () => { + expect( + AdrSlug.createFromTitle("My ADR", undefined, new Date(2020, 0, 1)).value + ).toEqual("20200101-my-adr"); + }); + + it("creates from title with complex title", () => { + expect( + AdrSlug.createFromTitle( + "L'exemple d'un titre compliqué ! @test", + undefined, + new Date(2020, 0, 1) + ).value + ).toEqual("20200101-lexemple-dun-titre-complique-test"); + }); + }); +}); diff --git a/packages/core/src/adr/domain/AdrSlug.ts b/packages/core/src/adr/domain/AdrSlug.ts new file mode 100644 index 00000000..1bde6f92 --- /dev/null +++ b/packages/core/src/adr/domain/AdrSlug.ts @@ -0,0 +1,58 @@ +import moment from "moment"; +import slugify from "slugify"; +import { Log4brainsError, ValueObject } from "@src/domain"; +import { AdrFile } from "./AdrFile"; +import { PackageRef } from "./PackageRef"; + +type Props = { + value: string; +}; + +export class AdrSlug extends ValueObject<Props> { + constructor(value: string) { + super({ value }); + + if (this.namePart.includes("/")) { + throw new Log4brainsError( + "The / character is not allowed in the name part of an ADR slug", + value + ); + } + } + + get value(): string { + return this.props.value; + } + + get packagePart(): string | undefined { + const s = this.value.split("/", 2); + return s.length >= 2 ? s[0] : undefined; + } + + get namePart(): string { + const s = this.value.split("/", 2); + return s.length >= 2 ? s[1] : s[0]; + } + + static createFromFile(file: AdrFile, packageRef?: PackageRef): AdrSlug { + const localSlug = file.path.basenameWithoutExtension; + return new AdrSlug( + packageRef ? `${packageRef.name}/${localSlug}` : localSlug + ); + } + + static createFromTitle( + title: string, + packageRef?: PackageRef, + date?: Date + ): AdrSlug { + const slugifiedTitle = slugify(title, { + lower: true, + strict: true + }).replace(/-*$/, ""); + const localSlug = `${moment(date).format("YYYYMMDD")}-${slugifiedTitle}`; + return new AdrSlug( + packageRef ? `${packageRef.name}/${localSlug}` : localSlug + ); + } +} diff --git a/packages/core/src/adr/domain/AdrStatus.test.ts b/packages/core/src/adr/domain/AdrStatus.test.ts new file mode 100644 index 00000000..995c206e --- /dev/null +++ b/packages/core/src/adr/domain/AdrStatus.test.ts @@ -0,0 +1,19 @@ +import { AdrStatus } from "./AdrStatus"; + +describe("AdrStatus", () => { + it("create from name", () => { + const status = AdrStatus.createFromName("draft"); + expect(status.name).toEqual("draft"); + }); + + it("throws when unknown name", () => { + expect(() => { + AdrStatus.createFromName("loremipsum"); + }).toThrow(); + }); + + it("works with 'superseded by XXX", () => { + const status = AdrStatus.createFromName("superseded by XXX"); + expect(status.name).toEqual("superseded"); + }); +}); diff --git a/packages/core/src/adr/domain/AdrStatus.ts b/packages/core/src/adr/domain/AdrStatus.ts new file mode 100644 index 00000000..b5901a53 --- /dev/null +++ b/packages/core/src/adr/domain/AdrStatus.ts @@ -0,0 +1,45 @@ +import { Log4brainsError, ValueObject } from "@src/domain"; + +type Props = { + name: string; +}; + +export class AdrStatus extends ValueObject<Props> { + static DRAFT = new AdrStatus("draft"); + + static PROPOSED = new AdrStatus("proposed"); + + static REJECTED = new AdrStatus("rejected"); + + static ACCEPTED = new AdrStatus("accepted"); + + static DEPRECATED = new AdrStatus("deprecated"); + + static SUPERSEDED = new AdrStatus("superseded"); + + private constructor(name: string) { + super({ name }); + } + + get name(): string { + return this.props.name; + } + + static createFromName(name: string): AdrStatus { + if (name.toLowerCase().startsWith("superseded by")) { + return this.SUPERSEDED; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const status = Object.values(AdrStatus) + .filter((prop) => { + return prop instanceof AdrStatus && prop.name === name.toLowerCase(); + }) + .pop(); + if (!status) { + throw new Log4brainsError("Unknown ADR status", name); + } + + return status as AdrStatus; + } +} diff --git a/packages/core/src/adr/domain/AdrTemplate.test.ts b/packages/core/src/adr/domain/AdrTemplate.test.ts new file mode 100644 index 00000000..97af8ae6 --- /dev/null +++ b/packages/core/src/adr/domain/AdrTemplate.test.ts @@ -0,0 +1,53 @@ +import { Adr } from "./Adr"; +import { AdrSlug } from "./AdrSlug"; +import { AdrTemplate } from "./AdrTemplate"; +import { MarkdownBody } from "./MarkdownBody"; +import { PackageRef } from "./PackageRef"; + +describe("AdrTemplate", () => { + beforeAll(() => { + Adr.setTz("Etc/UTC"); + }); + afterAll(() => { + Adr.clearTz(); + }); + + const tplMarkdown = `# [short title of solved problem and solution] + + - Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)] <!-- optional --> + - Deciders: [list everyone involved in the decision] <!-- optional --> + - Date: [YYYY-MM-DD when the decision was last updated] <!-- optional - changes the order displayed in the UI --> + + Technical Story: [description | ticket/issue URL] <!-- optional --> + + ## Context and Problem Statement + + [Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] +`; + + it("creates an ADR from the template", () => { + const template = new AdrTemplate({ + package: new PackageRef("test"), + body: new MarkdownBody(tplMarkdown) + }); + const adr = template.createAdrFromMe( + new AdrSlug("test/20200101-hello-world"), + "Hello World" + ); + expect(adr.slug.value).toEqual("test/20200101-hello-world"); + expect(adr.title).toEqual("Hello World"); + }); + + it("throws when package mismatch in slug", () => { + expect(() => { + const template = new AdrTemplate({ + package: new PackageRef("test"), + body: new MarkdownBody(tplMarkdown) + }); + template.createAdrFromMe( + new AdrSlug("other-package/20200101-hello-world"), + "Hello World" + ); + }).toThrow(); + }); +}); diff --git a/packages/core/src/adr/domain/AdrTemplate.ts b/packages/core/src/adr/domain/AdrTemplate.ts new file mode 100644 index 00000000..70a564fe --- /dev/null +++ b/packages/core/src/adr/domain/AdrTemplate.ts @@ -0,0 +1,42 @@ +import { AggregateRoot, Log4brainsError } from "@src/domain"; +import { Adr } from "./Adr"; +import { AdrSlug } from "./AdrSlug"; +import { MarkdownBody } from "./MarkdownBody"; +import { PackageRef } from "./PackageRef"; + +type Props = { + package?: PackageRef; + body: MarkdownBody; +}; + +export class AdrTemplate extends AggregateRoot<Props> { + get package(): PackageRef | undefined { + return this.props.package; + } + + get body(): MarkdownBody { + return this.props.body; + } + + createAdrFromMe(slug: AdrSlug, title: string): Adr { + const packageRef = slug.packagePart + ? new PackageRef(slug.packagePart) + : undefined; + if ( + (!this.package && packageRef) || + (this.package && !this.package.equals(packageRef)) + ) { + throw new Log4brainsError( + "The given slug does not match this template package name", + `slug: ${slug.value} / template package: ${this.package?.name}` + ); + } + const adr = new Adr({ + slug, + package: packageRef, + body: this.body.clone() + }); + adr.setTitle(title); + return adr; + } +} diff --git a/packages/core/src/adr/domain/Author.ts b/packages/core/src/adr/domain/Author.ts new file mode 100644 index 00000000..1b6bbdb5 --- /dev/null +++ b/packages/core/src/adr/domain/Author.ts @@ -0,0 +1,24 @@ +import { ValueObject } from "@src/domain"; + +type Props = { + name: string; + email?: string; +}; + +export class Author extends ValueObject<Props> { + constructor(name: string, email?: string) { + super({ name, email }); + } + + get name(): string { + return this.props.name; + } + + get email(): string | undefined { + return this.props.email; + } + + static createAnonymous(): Author { + return new Author("Anonymous"); + } +} diff --git a/packages/core/src/adr/domain/FilesystemPath.test.ts b/packages/core/src/adr/domain/FilesystemPath.test.ts new file mode 100644 index 00000000..7d06a8fa --- /dev/null +++ b/packages/core/src/adr/domain/FilesystemPath.test.ts @@ -0,0 +1,74 @@ +import { FilesystemPath } from "./FilesystemPath"; + +describe("FilesystemPath", () => { + it("throws when CWD is not absolute", () => { + expect(() => { + new FilesystemPath(".", "test"); + }).toThrow(); + }); + + it("returns the absolute path", () => { + expect(new FilesystemPath("/foo", "./bar/test").absolutePath).toEqual( + "/foo/bar/test" + ); + expect(new FilesystemPath("/foo", "bar/test").absolutePath).toEqual( + "/foo/bar/test" + ); + expect(new FilesystemPath("/foo/bar", "../test").absolutePath).toEqual( + "/foo/test" + ); + }); + + it("returns the basename", () => { + expect(new FilesystemPath("/foo", "bar/test").basename).toEqual("test"); + expect(new FilesystemPath("/foo", "bar/test.md").basename).toEqual( + "test.md" + ); + }); + + it("returns the extension", () => { + expect(new FilesystemPath("/foo", "bar/test").extension).toEqual(""); + expect(new FilesystemPath("/foo", "bar/test.md").extension).toEqual(".md"); + }); + + it("returns the basename without the extension", () => { + expect( + new FilesystemPath("/foo", "bar/test").basenameWithoutExtension + ).toEqual("test"); + expect( + new FilesystemPath("/foo", "bar/test.md").basenameWithoutExtension + ).toEqual("test"); + }); + + it("joins a FilesystemPath to a string path", () => { + expect( + new FilesystemPath("/foo", "bar/test").join("hello-world.md").absolutePath + ).toEqual("/foo/bar/test/hello-world.md"); + }); + + it("returns the relative path between two paths (from a directory)", () => { + const from = new FilesystemPath("/test", "foo/bar"); + expect(from.relative(new FilesystemPath("/test", "foo"), true)).toEqual( + ".." + ); + expect( + from.relative(new FilesystemPath("/test", "foo/lorem/ipsum"), true) + ).toEqual("../lorem/ipsum"); + expect( + from.relative(new FilesystemPath("/test", "foo/bar/test"), true) + ).toEqual("test"); + }); + + it("returns the relative path between two paths (from a file)", () => { + const from = new FilesystemPath("/test", "foo/bar.md"); + expect(from.relative(new FilesystemPath("/test", "foo"), false)).toEqual( + "" + ); + expect( + from.relative(new FilesystemPath("/test", "foo/lorem/ipsum"), false) + ).toEqual("lorem/ipsum"); + expect( + from.relative(new FilesystemPath("/test", "bar/test"), false) + ).toEqual("../bar/test"); + }); +}); diff --git a/packages/core/src/adr/domain/FilesystemPath.ts b/packages/core/src/adr/domain/FilesystemPath.ts new file mode 100644 index 00000000..b64d3851 --- /dev/null +++ b/packages/core/src/adr/domain/FilesystemPath.ts @@ -0,0 +1,77 @@ +import path from "path"; +import { Log4brainsError, ValueObject } from "@src/domain"; +import { forceUnixPath } from "@src/lib/paths"; + +type Props = { + cwdAbsolutePath: string; + pathRelativeToCwd: string; +}; + +export class FilesystemPath extends ValueObject<Props> { + constructor(cwdAbsolutePath: string, pathRelativeToCwd: string) { + super({ + cwdAbsolutePath: forceUnixPath(cwdAbsolutePath), + pathRelativeToCwd: forceUnixPath(pathRelativeToCwd) + }); + + if (!path.isAbsolute(cwdAbsolutePath)) { + throw new Log4brainsError("CWD path is not absolute", cwdAbsolutePath); + } + } + + get cwdAbsolutePath(): string { + return this.props.cwdAbsolutePath; + } + + get pathRelativeToCwd(): string { + return this.props.pathRelativeToCwd; + } + + get absolutePath(): string { + return forceUnixPath( + path.join(this.props.cwdAbsolutePath, this.pathRelativeToCwd) + ); + } + + get basename(): string { + return forceUnixPath(path.basename(this.pathRelativeToCwd)); + } + + get extension(): string { + // with the dot (.) + return path.extname(this.pathRelativeToCwd); + } + + get basenameWithoutExtension(): string { + if (!this.extension) { + return this.basename; + } + return this.basename.substring( + 0, + this.basename.length - this.extension.length + ); + } + + join(p: string): FilesystemPath { + return new FilesystemPath( + this.cwdAbsolutePath, + path.join(this.pathRelativeToCwd, p) + ); + } + + relative(to: FilesystemPath, amIaDirectory = false): string { + const from = amIaDirectory + ? this.absolutePath + : path.dirname(this.absolutePath); + return forceUnixPath(path.relative(from, to.absolutePath)); + } + + public equals(vo?: ValueObject<Props>): boolean { + // We redefine ValueObject's equals() method to test only the computed absolutePath + // because in some the pathRelativeToCwd can be different but targets the same location + if (vo === null || vo === undefined || !(vo instanceof FilesystemPath)) { + return false; + } + return this.absolutePath === vo.absolutePath; + } +} diff --git a/packages/core/src/adr/domain/MarkdownAdrLink.test.ts b/packages/core/src/adr/domain/MarkdownAdrLink.test.ts new file mode 100644 index 00000000..55a6e046 --- /dev/null +++ b/packages/core/src/adr/domain/MarkdownAdrLink.test.ts @@ -0,0 +1,36 @@ +import { Adr } from "./Adr"; +import { AdrFile } from "./AdrFile"; +import { AdrSlug } from "./AdrSlug"; +import { FilesystemPath } from "./FilesystemPath"; +import { MarkdownAdrLink } from "./MarkdownAdrLink"; +import { MarkdownBody } from "./MarkdownBody"; +import { PackageRef } from "./PackageRef"; + +describe("MarkdownAdrLink", () => { + beforeAll(() => { + Adr.setTz("Etc/UTC"); + }); + afterAll(() => { + Adr.clearTz(); + }); + + it("works with relative paths", () => { + const from = new Adr({ + slug: new AdrSlug("from"), + file: new AdrFile(new FilesystemPath("/", "docs/adr/from.md")), + body: new MarkdownBody("") + }); + const to = new Adr({ + slug: new AdrSlug("test/to"), + package: new PackageRef("test"), + file: new AdrFile( + new FilesystemPath("/", "packages/test/docs/adr/to.md") + ), + body: new MarkdownBody("") + }); + const link = new MarkdownAdrLink(from, to); + expect(link.toMarkdown()).toEqual( + "[test/to](../../packages/test/docs/adr/to.md)" + ); + }); +}); diff --git a/packages/core/src/adr/domain/MarkdownAdrLink.ts b/packages/core/src/adr/domain/MarkdownAdrLink.ts new file mode 100644 index 00000000..f6debe44 --- /dev/null +++ b/packages/core/src/adr/domain/MarkdownAdrLink.ts @@ -0,0 +1,32 @@ +import { Log4brainsError, ValueObject } from "@src/domain"; +import type { Adr } from "./Adr"; + +type Props = { + from: Adr; + to: Adr; +}; + +export class MarkdownAdrLink extends ValueObject<Props> { + constructor(from: Adr, to: Adr) { + super({ from, to }); + } + + get from(): Adr { + return this.props.from; + } + + get to(): Adr { + return this.props.to; + } + + toMarkdown(): string { + if (!this.from.file || !this.to.file) { + throw new Log4brainsError( + "Impossible to create a link between two unsaved ADRs", + `${this.from.slug.value} -> ${this.to.slug.value}` + ); + } + const relativePath = this.from.file.path.relative(this.to.file.path); + return `[${this.to.slug.value}](${relativePath})`; + } +} diff --git a/packages/core/src/adr/domain/MarkdownAdrLinkResolver.ts b/packages/core/src/adr/domain/MarkdownAdrLinkResolver.ts new file mode 100644 index 00000000..3dc57005 --- /dev/null +++ b/packages/core/src/adr/domain/MarkdownAdrLinkResolver.ts @@ -0,0 +1,6 @@ +import { Adr } from "./Adr"; +import { MarkdownAdrLink } from "./MarkdownAdrLink"; + +export interface MarkdownAdrLinkResolver { + resolve(from: Adr, uri: string): Promise<MarkdownAdrLink | undefined>; +} diff --git a/packages/core/src/adr/domain/MarkdownBody.test.ts b/packages/core/src/adr/domain/MarkdownBody.test.ts new file mode 100644 index 00000000..ace8c8de --- /dev/null +++ b/packages/core/src/adr/domain/MarkdownBody.test.ts @@ -0,0 +1,297 @@ +import { MarkdownBody } from "./MarkdownBody"; + +describe("MarkdownBody", () => { + describe("getFirstH1Title()", () => { + it("returns the first H1 title", () => { + const body = new MarkdownBody( + `# First title +Lorem ipsum +## Subtitle +## Subtitle +# Second title` + ); + expect(body.getFirstH1Title()).toEqual("First title"); + }); + + it("returns undefined when there is no first title", () => { + const body = new MarkdownBody( + `Lorem ipsum +## Subtitle +## Subtitle` + ); + expect(body.getFirstH1Title()).toBeUndefined(); + }); + }); + + describe("setFirstH1Title()", () => { + it("replaces the existing one", () => { + const body = new MarkdownBody( + `# First title +Lorem ipsum +## Subtitle +## Subtitle +# Second title` + ); + body.setFirstH1Title("New title"); + expect(body.getRawMarkdown()).toEqual(`# New title +Lorem ipsum +## Subtitle +## Subtitle +# Second title`); + }); + + it("creates one if needed", () => { + const body = new MarkdownBody( + `Lorem ipsum +## Subtitle +## Subtitle` + ); + body.setFirstH1Title("New title"); + expect(body.getRawMarkdown()).toEqual(`# New title +Lorem ipsum +## Subtitle +## Subtitle`); + }); + }); + + describe("getHeaderMetadata()", () => { + it("returns a metadata", () => { + const body = new MarkdownBody( + `# Hello World + +- Lorem Ipsum +- Status: draft +- DATE : 2020-01-01 + +Technical Story: [description | ticket/issue URL] <!-- optional --> +## Subtitle +## Subtitle +# Second title` + ); + expect(body.getHeaderMetadata("status")).toEqual("draft"); + expect(body.getHeaderMetadata("date")).toEqual("2020-01-01"); + }); + + it("returns a metadata even if there is a paragraph before", () => { + const body = new MarkdownBody( + `# Hello World +Hello! + +- Lorem Ipsum +- Status: draft +- DATE : 2020-01-01 + +Technical Story: [description | ticket/issue URL] <!-- optional --> +## Subtitle +## Subtitle +# Second title` + ); + expect(body.getHeaderMetadata("status")).toEqual("draft"); + expect(body.getHeaderMetadata("date")).toEqual("2020-01-01"); + }); + + it("returns undefined when the metadata is not set", () => { + const body = new MarkdownBody( + `# Hello World +Hello! + +- Lorem Ipsum +- Status: draft +- DATE : 2020-01-01 + +Technical Story: [description | ticket/issue URL] <!-- optional --> +## Subtitle +## Subtitle +# Second title` + ); + expect(body.getHeaderMetadata("Deciders")).toBeUndefined(); + }); + }); + + describe("setHeaderMetadata()", () => { + it("modifies an already existing metadata", () => { + const body = new MarkdownBody( + `# Hello World +Hello! + +- Lorem Ipsum +- Status: draft +- DATE : 2020-01-01 + +Technical Story: [description | ticket/issue URL] <!-- optional --> +## Subtitle +## Subtitle +# Second title` + ); + + body.setHeaderMetadata("Status", "accepted"); + + expect(body.getRawMarkdown()).toEqual(`# Hello World +Hello! + +- Lorem Ipsum +- Status: accepted +- DATE : 2020-01-01 + +Technical Story: [description | ticket/issue URL] <!-- optional --> +## Subtitle +## Subtitle +# Second title`); + }); + + it("creates a metadata", () => { + const body = new MarkdownBody( + `# Hello World +Hello! + +- Lorem Ipsum +- Status: draft +- DATE : 2020-01-01 + +Technical Story: [description | ticket/issue URL] <!-- optional --> +## Subtitle +## Subtitle +# Second title` + ); + + body.setHeaderMetadata("Deciders", "@JohnDoe"); + + expect(body.getRawMarkdown()).toEqual(`# Hello World +Hello! + +- Lorem Ipsum +- Status: draft +- DATE : 2020-01-01 +- Deciders: @JohnDoe + +Technical Story: [description | ticket/issue URL] <!-- optional --> +## Subtitle +## Subtitle +# Second title`); + }); + + it("creates a metadata even if the paragraph does not exist", () => { + const body = new MarkdownBody( + `# Hello World + +## Subtitle +## Subtitle +# Second title` + ); + + body.setHeaderMetadata("Deciders", "@JohnDoe"); + + expect(body.getRawMarkdown()).toEqual(`# Hello World + +- Deciders: @JohnDoe + + +## Subtitle +## Subtitle +# Second title`); + }); + }); + + describe("getlinks()", () => { + it("returns links", () => { + const body = new MarkdownBody( + `# Hello World +## Subtitle + +- test + +## Subtitle +## Links + +- link1: [foo](bar.md) +- link2` + ); + expect(body.getLinks()).toEqual(["link1: [foo](bar.md)", "link2"]); + }); + + it("returns undefined when no links", () => { + const body = new MarkdownBody( + `# Hello World +## Subtitle + +- test + +## Subtitle` + ); + expect(body.getLinks()).toBeUndefined(); + }); + }); + + describe("addLink()", () => { + it("adds a link", () => { + const body = new MarkdownBody( + `# Hello World +## Subtitle +## Links + +- link1: [foo](bar.md) +- link2 + +` + ); // TODO: fix this whitespace issue + + body.addLink("link3"); + + expect(body.getRawMarkdown()).toEqual(`# Hello World +## Subtitle +## Links + +- link1: [foo](bar.md) +- link2 +- link3 + +`); + }); + + it("adds a link even if the paragraph does not exist", () => { + const body = new MarkdownBody( + `# Hello World +## Subtitle +Lorem ipsum` + ); // TODO: fix this whitespace issue + + body.addLink("link1"); + + expect(body.getRawMarkdown()).toEqual(`# Hello World +## Subtitle +Lorem ipsum + +## Links + +- link1 + +`); + }); + }); + + describe("addLinkNoDuplicate()", () => { + it("does not add the link if there is a duplicate", () => { + const body = new MarkdownBody( + `# Hello World +## Subtitle +## Links + +- link test + +` + ); // TODO: fix this whitespace issue + + body.addLinkNoDuplicate("Link TEST"); + body.addLinkNoDuplicate("Link2"); + + expect(body.getRawMarkdown()).toEqual(`# Hello World +## Subtitle +## Links + +- link test +- Link2 + +`); + }); + }); +}); diff --git a/packages/core/src/adr/domain/MarkdownBody.ts b/packages/core/src/adr/domain/MarkdownBody.ts new file mode 100644 index 00000000..5aa9e8ab --- /dev/null +++ b/packages/core/src/adr/domain/MarkdownBody.ts @@ -0,0 +1,248 @@ +import cheerio from "cheerio"; +import { Entity, Log4brainsError } from "@src/domain"; +import { CheerioMarkdown, cheerioToMarkdown } from "@src/lib/cheerio-markdown"; +import type { Adr } from "./Adr"; +import { MarkdownAdrLinkResolver } from "./MarkdownAdrLinkResolver"; + +type Props = { + value: string; +}; + +type ElementAndRegExpMatch = { + element: cheerio.Cheerio; + match: string[]; +}; + +type Link = { + text: string; + href: string; +}; + +function htmlentities(str: string): string { + return str + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """); +} + +export class MarkdownBody extends Entity<Props> { + private cm: CheerioMarkdown; + + private adrLinkResolver?: MarkdownAdrLinkResolver; + + constructor(value: string) { + super({ value }); + this.cm = new CheerioMarkdown(value); + this.cm.onChange((newValue) => { + this.props.value = newValue; + }); + } + + setAdrLinkResolver(resolver: MarkdownAdrLinkResolver): MarkdownBody { + this.adrLinkResolver = resolver; + return this; + } + + private getFirstH1TitleElement(): cheerio.Cheerio | undefined { + const elt = this.cm.$("h1").first(); + return elt.length > 0 ? elt : undefined; + } + + getFirstH1Title(): string | undefined { + return this.getFirstH1TitleElement()?.text(); + } + + setFirstH1Title(title: string): void { + const elt = this.getFirstH1TitleElement(); + if (elt) { + this.cm.replaceText(elt, title); + } else { + this.cm.insertLineAt(0, `# ${title}`); + } + } + + deleteFirstH1Title(): void { + const elt = this.getFirstH1TitleElement(); + if (elt) { + this.cm.deleteElement(elt); + } + } + + private getHeaderMetadataUl(): cheerio.Cheerio | undefined { + const elts = this.cm.$("body > *:first-child").nextUntil("h2").addBack(); + const ul = elts.filter("ul").first(); + return ul.length > 0 ? ul : undefined; + } + + private getHeaderMetadataElementAndMatch( + key: string + ): ElementAndRegExpMatch | undefined { + const ul = this.getHeaderMetadataUl(); + if (!ul) { + return undefined; + } + const regexp = new RegExp(`^(\\s*${key}\\s*:\\s*)(.*)$`, "i"); + const result = ul + .children() + .map((i, li) => { + const line = this.cm.$(li); + const match = regexp.exec(line.text()); + return match ? { element: this.cm.$(li), match } : undefined; + }) + .get() as ElementAndRegExpMatch[]; + return result[0] ?? undefined; + } + + getHeaderMetadata(key: string): string | undefined { + return this.getHeaderMetadataElementAndMatch(key)?.match[2].trim(); + } + + setHeaderMetadata(key: string, value: string): void { + const res = this.getHeaderMetadataElementAndMatch(key); + if (res) { + this.cm.replaceText(res.element, `${res.match[1]}${value}`); + } else { + const ul = this.getHeaderMetadataUl(); + if (ul) { + this.cm.appendToList(ul, `${key}: ${value}`); + } else { + const h1TitleElt = this.getFirstH1TitleElement(); + if (h1TitleElt) { + this.cm.insertLineAfter(h1TitleElt, `\n- ${key}: ${value}\n`); + } else { + this.cm.insertLineAt(0, `- ${key}: ${value}`); + } + } + } + } + + deleteHeaderMetadata(key: string): void { + // TODO: fix bug: when the last item is deleted, it deletes also the next new line. + // As a result, it is not detected as a list anymore. + const res = this.getHeaderMetadataElementAndMatch(key); + if (res) { + this.cm.deleteElement(res.element); + } + } + + private getLinksUl(): cheerio.Cheerio | undefined { + const h2Results = this.cm.$("h2").filter( + (i, elt) => + this.cm + .$(elt) + .text() + .toLowerCase() + .replace(/<!--.*-->/, "") + .trim() === "links" + ); + if (h2Results.length === 0) { + return undefined; + } + const h2 = h2Results[0]; + const elts = this.cm.$(h2).nextUntil("h2"); + const ul = elts.filter("ul").first(); + return ul.length > 0 ? ul : undefined; + } + + getLinks(): string[] | undefined { + const ul = this.getLinksUl(); + if (!ul) { + return undefined; + } + return ul + .children() + .map((i, li) => cheerioToMarkdown(this.cm.$(li))) + .get() as string[]; + } + + addLink(link: string): void { + const ul = this.getLinksUl(); + if (ul === undefined) { + this.cm.appendLine(`\n## Links\n\n- ${link}`); + } else { + this.cm.appendToList(ul, link); + } + } + + addLinkNoDuplicate(link: string): void { + const links = this.getLinks(); + if ( + links && + links + .map((l) => l.toLowerCase().trim()) + .filter((l) => l === link.toLowerCase().trim()).length > 0 + ) { + return; + } + this.addLink(link); + } + + getRawMarkdown(): string { + return this.props.value; + } + + clone(): MarkdownBody { + const copy = new MarkdownBody(this.props.value); + if (this.adrLinkResolver) { + copy.setAdrLinkResolver(this.adrLinkResolver); + } + return copy; + } + + async replaceAdrLinks(from: Adr): Promise<void> { + const links = this.cm + .$("a") + .map((_, element) => ({ + text: this.cm.$(element).text(), + href: this.cm.$(element).attr("href") + })) + .get() as Link[]; + + const isUrlRegexp = new RegExp(/^https?:\/\//i); + + const promises = links + .filter((link) => !isUrlRegexp.exec(link.href)) + .filter((link) => link.href.toLowerCase().endsWith(".md")) + .map((link) => + (async () => { + if (!this.adrLinkResolver) { + throw new Log4brainsError( + "Impossible to call replaceAdrLinks() without an MarkdownAdrLinkResolver" + ); + } + const mdAdrLink = await this.adrLinkResolver.resolve(from, link.href); + if (mdAdrLink) { + const params = [ + `slug="${htmlentities(mdAdrLink.to.slug.value)}"`, + `status="${mdAdrLink.to.status.name}"` + ]; + if (mdAdrLink.to.title) { + params.push(`title="${htmlentities(mdAdrLink.to.title)}"`); + } + if (mdAdrLink.to.package) { + params.push( + `package="${htmlentities(mdAdrLink.to.package.name)}"` + ); + } + if ( + ![ + mdAdrLink.to.slug.value.toLowerCase(), + mdAdrLink.to.slug.namePart.toLowerCase() + ].includes(link.text.toLowerCase().trim()) + ) { + params.push(`customLabel="${htmlentities(link.text)}"`); + } + this.cm.updateMarkdown( + this.cm.markdown.replace( + `[${link.text}](${link.href})`, + `<AdrLink ${params.join(" ")} />` + ) + ); + } + })() + ); + + await Promise.all(promises); + } +} diff --git a/packages/core/src/adr/domain/Package.ts b/packages/core/src/adr/domain/Package.ts new file mode 100644 index 00000000..1783e75c --- /dev/null +++ b/packages/core/src/adr/domain/Package.ts @@ -0,0 +1,23 @@ +import { Entity } from "@src/domain"; +import { FilesystemPath } from "./FilesystemPath"; +import { PackageRef } from "./PackageRef"; + +type Props = { + ref: PackageRef; + path: FilesystemPath; + adrFolderPath: FilesystemPath; +}; + +export class Package extends Entity<Props> { + get ref(): PackageRef { + return this.props.ref; + } + + get path(): FilesystemPath { + return this.props.path; + } + + get adrFolderPath(): FilesystemPath { + return this.props.adrFolderPath; + } +} diff --git a/packages/core/src/adr/domain/PackageRef.ts b/packages/core/src/adr/domain/PackageRef.ts new file mode 100644 index 00000000..2cc522aa --- /dev/null +++ b/packages/core/src/adr/domain/PackageRef.ts @@ -0,0 +1,15 @@ +import { ValueObject } from "@src/domain"; + +type Props = { + name: string; +}; + +export class PackageRef extends ValueObject<Props> { + constructor(name: string) { + super({ name }); + } + + get name(): string { + return this.props.name; + } +} diff --git a/packages/core/src/adr/domain/index.ts b/packages/core/src/adr/domain/index.ts new file mode 100644 index 00000000..2404da00 --- /dev/null +++ b/packages/core/src/adr/domain/index.ts @@ -0,0 +1,13 @@ +export * from "./Adr"; +export * from "./AdrFile"; +export * from "./AdrRelation"; +export * from "./AdrSlug"; +export * from "./AdrStatus"; +export * from "./AdrTemplate"; +export * from "./Author"; +export * from "./FilesystemPath"; +export * from "./MarkdownAdrLink"; +export * from "./MarkdownAdrLinkResolver"; +export * from "./MarkdownBody"; +export * from "./Package"; +export * from "./PackageRef"; diff --git a/packages/core/src/adr/infrastructure/MarkdownAdrLinkResolver.ts b/packages/core/src/adr/infrastructure/MarkdownAdrLinkResolver.ts new file mode 100644 index 00000000..8ca13859 --- /dev/null +++ b/packages/core/src/adr/infrastructure/MarkdownAdrLinkResolver.ts @@ -0,0 +1,40 @@ +import { + Adr, + AdrFile, + MarkdownAdrLink, + MarkdownAdrLinkResolver as IMarkdownAdrLinkResolver +} from "@src/adr/domain"; +import { Log4brainsError } from "@src/domain"; +import type { AdrRepository } from "./repositories"; + +type Deps = { + adrRepository: AdrRepository; +}; + +export class MarkdownAdrLinkResolver implements IMarkdownAdrLinkResolver { + private readonly adrRepository: AdrRepository; + + constructor({ adrRepository }: Deps) { + this.adrRepository = adrRepository; + } + + async resolve(from: Adr, uri: string): Promise<MarkdownAdrLink | undefined> { + if (!from.file) { + throw new Log4brainsError( + "Impossible to resolve links on an non-saved ADR" + ); + } + + const path = from.file.path.join("..").join(uri); + if (!AdrFile.isPathValid(path)) { + return undefined; + } + + const to = await this.adrRepository.findFromFile(new AdrFile(path)); + if (!to) { + return undefined; + } + + return new MarkdownAdrLink(from, to); + } +} diff --git a/packages/core/src/adr/infrastructure/repositories/AdrRepository.ts b/packages/core/src/adr/infrastructure/repositories/AdrRepository.ts new file mode 100644 index 00000000..ed72274e --- /dev/null +++ b/packages/core/src/adr/infrastructure/repositories/AdrRepository.ts @@ -0,0 +1,301 @@ +/* eslint-disable class-methods-use-this */ +import fs, { promises as fsP } from "fs"; +import path from "path"; +import simpleGit, { SimpleGit } from "simple-git"; +import { AdrRepository as IAdrRepository } from "@src/adr/application"; +import { + Adr, + AdrFile, + AdrSlug, + Author, + FilesystemPath, + MarkdownBody, + PackageRef +} from "@src/adr/domain"; +import { Log4brainsConfig } from "@src/infrastructure/config"; +import { Log4brainsError } from "@src/domain"; +import { PackageRepository } from "./PackageRepository"; +import { MarkdownAdrLinkResolver } from "../MarkdownAdrLinkResolver"; + +type GitMetadata = { + creationDate: Date; + lastEditDate: Date; + lastEditAuthor: Author; +}; + +type Deps = { + config: Log4brainsConfig; + workdir: string; + packageRepository: PackageRepository; +}; + +export class AdrRepository implements IAdrRepository { + private readonly config: Log4brainsConfig; + + private readonly workdir: string; + + private readonly packageRepository: PackageRepository; + + private readonly git: SimpleGit; + + private gitAvailable?: boolean; + + private anonymousAuthor?: Author; + + private readonly markdownAdrLinkResolver: MarkdownAdrLinkResolver; + + constructor({ config, workdir, packageRepository }: Deps) { + this.config = config; + this.workdir = workdir; + this.packageRepository = packageRepository; + this.git = simpleGit({ baseDir: workdir }); + this.markdownAdrLinkResolver = new MarkdownAdrLinkResolver({ + adrRepository: this + }); + } + + private async isGitAvailable(): Promise<boolean> { + if (this.gitAvailable === undefined) { + try { + this.gitAvailable = await this.git.checkIsRepo(); + } catch (e) { + this.gitAvailable = false; + } + } + return this.gitAvailable; + } + + async find(slug: AdrSlug): Promise<Adr> { + const packageRef = this.getPackageRef(slug); + const adr = await this.findInPath( + slug, + this.getAdrFolderPath(packageRef), + packageRef + ); + if (!adr) { + throw new Log4brainsError("This ADR does not exist", slug.value); + } + return adr; + } + + async findFromFile(adrFile: AdrFile): Promise<Adr | undefined> { + const adrFolderPath = adrFile.path.join(".."); + const pkg = this.packageRepository.findByAdrFolderPath(adrFolderPath); + const possibleSlug = AdrSlug.createFromFile( + adrFile, + pkg ? pkg.ref : undefined + ); + + try { + return await this.find(possibleSlug); + } catch (e) { + // ignore + } + return undefined; + } + + async findAll(): Promise<Adr[]> { + const packages = this.packageRepository.findAll(); + return ( + await Promise.all([ + this.findAllInPath(this.getAdrFolderPath()), + ...packages.map((pkg) => { + return this.findAllInPath(pkg.adrFolderPath, pkg.ref); + }) + ]) + ) + .flat() + .sort(Adr.compare); + } + + private async getGitMetadata( + file: AdrFile + ): Promise<GitMetadata | undefined> { + if (!(await this.isGitAvailable())) { + return undefined; + } + + let logs; + let retry = 0; + do { + // eslint-disable-next-line no-await-in-loop + logs = (await this.git.log([file.path.absolutePath])).all; + + // TODO: debug this strange bug + // Sometimes, especially during snapshot testing, the `git log` command retruns nothing. + // And after a second retry, it works. + // Impossible to find out why for now, and since it causes a lot of false positive in the integration tests, + // we had to implement this quickfix + retry += 1; + } while (logs.length === 0 && retry <= 1); + + if (logs.length === 0) { + return undefined; + } + + return { + creationDate: new Date(logs[logs.length - 1].date), + lastEditDate: new Date(logs[0].date), + lastEditAuthor: new Author(logs[0].author_name, logs[0].author_email) + }; + } + + /** + * In preview mode, we set the Anonymous author as the current Git `user.name` global config. + * It should not append in CI. But if this is the case, it will appear as "Anonymous". + * Response is cached. + */ + private async getAnonymousAuthor(): Promise<Author> { + if (!this.anonymousAuthor) { + this.anonymousAuthor = Author.createAnonymous(); + if (await this.isGitAvailable()) { + const config = await this.git.listConfig(); + if (config?.all["user.name"]) { + this.anonymousAuthor = new Author( + config.all["user.name"] as string, + config.all["user.email"] as string | undefined + ); + } + } + } + return this.anonymousAuthor; + } + + private async getLastEditDateFromFilesystem(file: AdrFile): Promise<Date> { + const stat = await fsP.stat(file.path.absolutePath); + return stat.mtime; + } + + private async findInPath( + slug: AdrSlug, + p: FilesystemPath, + packageRef?: PackageRef + ): Promise<Adr | undefined> { + return ( + await this.findAllInPath(p, packageRef, (f: AdrFile, s: AdrSlug) => + s.equals(slug) + ) + ).pop(); + } + + private async findAllInPath( + p: FilesystemPath, + packageRef?: PackageRef, + filter?: (f: AdrFile, s: AdrSlug) => boolean + ): Promise<Adr[]> { + const files = await fsP.readdir(p.absolutePath); + return Promise.all( + files + .map((filename) => { + return new FilesystemPath( + p.cwdAbsolutePath, + path.join(p.pathRelativeToCwd, filename) + ); + }) + .filter((fsPath) => { + return AdrFile.isPathValid(fsPath); + }) + .map((fsPath) => { + const adrFile = new AdrFile(fsPath); + const slug = AdrSlug.createFromFile(adrFile, packageRef); + return { adrFile, slug }; + }) + .filter(({ adrFile, slug }) => { + if (filter) { + return filter(adrFile, slug); + } + return true; + }) + .map(({ adrFile, slug }) => { + return fsP + .readFile(adrFile.path.absolutePath, { + encoding: "utf8" + }) + .then(async (markdown) => { + const baseAdrProps = { + slug, + package: packageRef, + body: new MarkdownBody(markdown).setAdrLinkResolver( + this.markdownAdrLinkResolver + ), + file: adrFile + }; + + // The file is versionned in Git + const gitMetadata = await this.getGitMetadata(adrFile); + if (gitMetadata) { + return new Adr({ + ...baseAdrProps, + creationDate: gitMetadata.creationDate, + lastEditDate: gitMetadata.lastEditDate, + lastEditAuthor: gitMetadata.lastEditAuthor + }); + } + + // The file is not versionned in Git yet + // So we rely on filesystem's last edit date and global git config + const lastEditDate = await this.getLastEditDateFromFilesystem( + adrFile + ); + return new Adr({ + ...baseAdrProps, + creationDate: lastEditDate, + lastEditDate, + lastEditAuthor: await this.getAnonymousAuthor() + }); + }); + }) + ); + } + + generateAvailableSlug(title: string, packageRef?: PackageRef): AdrSlug { + const adrFolderPath = this.getAdrFolderPath(packageRef); + const baseSlug = AdrSlug.createFromTitle(title, packageRef); + + let i = 1; + let slug: AdrSlug; + let filename: string; + do { + slug = new AdrSlug(`${baseSlug.value}${i > 1 ? `-${i}` : ""}`); + filename = `${slug.namePart}.md`; + i += 1; + } while (fs.existsSync(path.join(adrFolderPath.absolutePath, filename))); + + return slug; + } + + private getPackageRef(slug: AdrSlug): PackageRef | undefined { + // undefined if global + return slug.packagePart ? new PackageRef(slug.packagePart) : undefined; + } + + private getAdrFolderPath(packageRef?: PackageRef): FilesystemPath { + const pkg = packageRef + ? this.packageRepository.find(packageRef) + : undefined; + const cwd = path.resolve(this.workdir); + return pkg + ? pkg.adrFolderPath + : new FilesystemPath(cwd, this.config.project.adrFolder); + } + + async save(adr: Adr): Promise<void> { + let { file } = adr; + if (!file) { + file = AdrFile.createFromSlugInFolder( + this.getAdrFolderPath(adr.package), + adr.slug + ); + if (fs.existsSync(file.path.absolutePath)) { + throw new Log4brainsError( + "An ADR with this slug already exists", + adr.slug.value + ); + } + adr.setFile(file); + } + await fsP.writeFile(file.path.absolutePath, adr.body.getRawMarkdown(), { + encoding: "utf-8" + }); + } +} diff --git a/packages/core/src/adr/infrastructure/repositories/AdrTemplateRepository.ts b/packages/core/src/adr/infrastructure/repositories/AdrTemplateRepository.ts new file mode 100644 index 00000000..5a647871 --- /dev/null +++ b/packages/core/src/adr/infrastructure/repositories/AdrTemplateRepository.ts @@ -0,0 +1,70 @@ +/* eslint-disable class-methods-use-this */ +import fs, { promises as fsP } from "fs"; +import path from "path"; +import { AdrTemplateRepository as IAdrTemplateRepository } from "@src/adr/application"; +import { + AdrTemplate, + FilesystemPath, + MarkdownBody, + PackageRef +} from "@src/adr/domain"; +import { Log4brainsConfig } from "@src/infrastructure/config"; +import { Log4brainsError } from "@src/domain"; +import { PackageRepository } from "./PackageRepository"; + +type Deps = { + config: Log4brainsConfig; + workdir: string; + packageRepository: PackageRepository; +}; + +export class AdrTemplateRepository implements IAdrTemplateRepository { + private readonly config: Log4brainsConfig; + + private readonly workdir: string; + + private readonly packageRepository: PackageRepository; + + constructor({ config, workdir, packageRepository }: Deps) { + this.config = config; + this.workdir = workdir; + this.packageRepository = packageRepository; + } + + async find(packageRef?: PackageRef): Promise<AdrTemplate> { + const adrFolderPath = this.getAdrFolderPath(packageRef); + const templatePath = path.join(adrFolderPath.absolutePath, "template.md"); + if (!fs.existsSync(templatePath)) { + if (packageRef) { + // Returns the global template when there is no custom template for a package + const globalTemplate = await this.find(); + return new AdrTemplate({ + package: packageRef, + body: globalTemplate.body + }); + } + throw new Log4brainsError( + "The template.md file does not exist", + path.join(adrFolderPath.pathRelativeToCwd, "template.md") + ); + } + + const markdown = await fsP.readFile(templatePath, { + encoding: "utf8" + }); + return new AdrTemplate({ + package: packageRef, + body: new MarkdownBody(markdown) + }); + } + + private getAdrFolderPath(packageRef?: PackageRef): FilesystemPath { + const pkg = packageRef + ? this.packageRepository.find(packageRef) + : undefined; + const cwd = path.resolve(this.workdir); + return pkg + ? pkg.adrFolderPath + : new FilesystemPath(cwd, this.config.project.adrFolder); + } +} diff --git a/packages/core/src/adr/infrastructure/repositories/PackageRepository.ts b/packages/core/src/adr/infrastructure/repositories/PackageRepository.ts new file mode 100644 index 00000000..2e9bef4d --- /dev/null +++ b/packages/core/src/adr/infrastructure/repositories/PackageRepository.ts @@ -0,0 +1,87 @@ +import path from "path"; +import fs from "fs"; +import { FilesystemPath, Package, PackageRef } from "@src/adr/domain"; +import { Log4brainsConfig } from "@src/infrastructure/config"; +import { Log4brainsError } from "@src/domain"; + +type Deps = { + config: Log4brainsConfig; + workdir: string; +}; + +export class PackageRepository { + private readonly config: Log4brainsConfig; + + private readonly workdir: string; + + // We cache findAll() results to avoid unnecessary I/O checks + private packages?: Package[]; + + constructor({ config, workdir }: Deps) { + this.config = config; + this.workdir = workdir; + } + + find(packageRef: PackageRef): Package { + const pkg = this.findAll() + .filter((p) => p.ref.equals(packageRef)) + .pop(); + if (!pkg) { + throw new Log4brainsError( + "No entry in the configuration for this package", + packageRef.name + ); + } + return pkg; + } + + findByAdrFolderPath(adrFolderPath: FilesystemPath): Package | undefined { + return this.findAll() + .filter((p) => p.adrFolderPath.equals(adrFolderPath)) + .pop(); + } + + findAll(): Package[] { + if (!this.packages) { + this.packages = ( + this.config.project.packages || [] + ).map((packageConfig) => + this.buildPackage( + packageConfig.name, + packageConfig.path, + packageConfig.adrFolder + ) + ); + } + return this.packages; + } + + private buildPackage( + name: string, + projectPath: string, + adrFolder: string + ): Package { + const cwd = path.resolve(this.workdir); + const pkg = new Package({ + ref: new PackageRef(name), + path: new FilesystemPath(cwd, projectPath), + adrFolderPath: new FilesystemPath(cwd, adrFolder) + }); + + if (!fs.existsSync(pkg.path.absolutePath)) { + throw new Log4brainsError( + "Package path does not exist", + `${pkg.path.pathRelativeToCwd} (${pkg.ref.name})` + ); + } + + if (!fs.existsSync(pkg.adrFolderPath.absolutePath)) { + throw new Log4brainsError( + "Package ADR folder path does not exist", + `${pkg.adrFolderPath.pathRelativeToCwd} (${pkg.ref.name})` + ); + } + + return pkg; + } +} diff --git a/packages/core/src/adr/infrastructure/repositories/index.ts b/packages/core/src/adr/infrastructure/repositories/index.ts new file mode 100644 index 00000000..505db30d --- /dev/null +++ b/packages/core/src/adr/infrastructure/repositories/index.ts @@ -0,0 +1,3 @@ +export * from "./AdrRepository"; +export * from "./AdrTemplateRepository"; +export * from "./PackageRepository"; diff --git a/packages/core/src/application/Command.ts b/packages/core/src/application/Command.ts new file mode 100644 index 00000000..90744453 --- /dev/null +++ b/packages/core/src/application/Command.ts @@ -0,0 +1 @@ +export abstract class Command {} diff --git a/packages/core/src/application/CommandHandler.ts b/packages/core/src/application/CommandHandler.ts new file mode 100644 index 00000000..28663eaf --- /dev/null +++ b/packages/core/src/application/CommandHandler.ts @@ -0,0 +1,8 @@ +import { Command } from "./Command"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface CommandHandler<C extends Command = any> { + // eslint-disable-next-line @typescript-eslint/ban-types + readonly commandClass: Function; + execute(command: C): Promise<void>; +} diff --git a/packages/core/src/application/Query.ts b/packages/core/src/application/Query.ts new file mode 100644 index 00000000..1acca031 --- /dev/null +++ b/packages/core/src/application/Query.ts @@ -0,0 +1 @@ +export abstract class Query {} diff --git a/packages/core/src/application/QueryHandler.ts b/packages/core/src/application/QueryHandler.ts new file mode 100644 index 00000000..8b3396e4 --- /dev/null +++ b/packages/core/src/application/QueryHandler.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Query } from "./Query"; + +export interface QueryHandler<Q extends Query = any, QR = any> { + // eslint-disable-next-line @typescript-eslint/ban-types + readonly queryClass: Function; + execute(query: Q): Promise<QR>; +} diff --git a/packages/core/src/application/index.ts b/packages/core/src/application/index.ts new file mode 100644 index 00000000..c21d020c --- /dev/null +++ b/packages/core/src/application/index.ts @@ -0,0 +1,4 @@ +export * from "./Command"; +export * from "./CommandHandler"; +export * from "./Query"; +export * from "./QueryHandler"; diff --git a/packages/core/src/decs.d.ts b/packages/core/src/decs.d.ts new file mode 100644 index 00000000..ffa330a7 --- /dev/null +++ b/packages/core/src/decs.d.ts @@ -0,0 +1,12 @@ +declare module "markdown-it-source-map"; + +declare module "launch-editor" { + export default function ( + path: string, + specifiedEditor?: string, + onErrorCallback?: ( + filename: string, + message?: string + ) => void | Promise<void> + ): void; +} diff --git a/packages/core/src/domain/AggregateRoot.ts b/packages/core/src/domain/AggregateRoot.ts new file mode 100644 index 00000000..b06b53c4 --- /dev/null +++ b/packages/core/src/domain/AggregateRoot.ts @@ -0,0 +1,5 @@ +import { Entity } from "./Entity"; + +export abstract class AggregateRoot< + T extends Record<string, unknown> +> extends Entity<T> {} diff --git a/packages/core/src/domain/Entity.ts b/packages/core/src/domain/Entity.ts new file mode 100644 index 00000000..f1940477 --- /dev/null +++ b/packages/core/src/domain/Entity.ts @@ -0,0 +1,7 @@ +export abstract class Entity<T extends Record<string, unknown>> { + constructor(public readonly props: T) {} + + public equals(e?: Entity<T>): boolean { + return e === this; // One instance allowed per entity + } +} diff --git a/packages/core/src/domain/Log4brainsError.ts b/packages/core/src/domain/Log4brainsError.ts new file mode 100644 index 00000000..e36fdf79 --- /dev/null +++ b/packages/core/src/domain/Log4brainsError.ts @@ -0,0 +1,9 @@ +/** + * Log4brains Error base class. + * Any error thrown by the core API extends this class. + */ +export class Log4brainsError extends Error { + constructor(public readonly name: string, public readonly details?: string) { + super(`${name}${details ? ` (${details})` : ""}`); + } +} diff --git a/packages/core/src/domain/ValueObject.test.ts b/packages/core/src/domain/ValueObject.test.ts new file mode 100644 index 00000000..8596d96c --- /dev/null +++ b/packages/core/src/domain/ValueObject.test.ts @@ -0,0 +1,49 @@ +import { ValueObject } from "./ValueObject"; + +describe("ValueObject", () => { + type MyVo1Props = { + prop1: string; + prop2: number; + }; + class MyVo1 extends ValueObject<MyVo1Props> {} + class MyVo1bis extends ValueObject<MyVo1Props> {} + + type MyVo2Props = { + prop1: string; + }; + class MyVo2 extends ValueObject<MyVo2Props> {} + + describe("equals()", () => { + it("returns true for the same instance", () => { + const vo = new MyVo1({ prop1: "foo", prop2: 42 }); + expect(vo.equals(vo)).toBeTruthy(); + }); + + it("returns true for a different instance with the same props", () => { + const vo1 = new MyVo1({ prop1: "foo", prop2: 42 }); + const vo2 = new MyVo1({ prop1: "foo", prop2: 42 }); + expect(vo1.equals(vo2)).toBeTruthy(); + expect(vo2.equals(vo1)).toBeTruthy(); + }); + + it("returns false for a different instance", () => { + const vo1 = new MyVo1({ prop1: "foo", prop2: 42 }); + const vo2 = new MyVo1({ prop1: "bar", prop2: 42 }); + expect(vo1.equals(vo2)).toBeFalsy(); + expect(vo2.equals(vo1)).toBeFalsy(); + }); + + it("returns false for a different class", () => { + const vo1 = new MyVo1({ prop1: "foo", prop2: 42 }); + const vo2 = new MyVo2({ prop1: "foo" }); + expect(vo2.equals(vo1)).toBeFalsy(); + }); + + it("returns false for a different class with the same props", () => { + const vo1 = new MyVo1({ prop1: "foo", prop2: 42 }); + const vo1bis = new MyVo1bis({ prop1: "foo", prop2: 42 }); + expect(vo1bis.equals(vo1)).toBeFalsy(); + expect(vo1.equals(vo1bis)).toBeFalsy(); + }); + }); +}); diff --git a/packages/core/src/domain/ValueObject.ts b/packages/core/src/domain/ValueObject.ts new file mode 100644 index 00000000..5af58824 --- /dev/null +++ b/packages/core/src/domain/ValueObject.ts @@ -0,0 +1,31 @@ +import isEqual from "lodash/isEqual"; + +// Inspired from https://khalilstemmler.com/articles/typescript-value-object/ +// Thank you :-) + +export type ValueObjectProps = Record<string, unknown>; + +/** + * @desc ValueObjects are objects that we determine their + * equality through their structural property. + */ +export abstract class ValueObject<T extends ValueObjectProps> { + public readonly props: T; + + constructor(props: T) { + this.props = Object.freeze(props); + } + + public equals(vo?: ValueObject<T>): boolean { + if (vo === null || vo === undefined) { + return false; + } + if (vo.constructor.name !== this.constructor.name) { + return false; + } + if (vo.props === undefined) { + return false; + } + return isEqual(this.props, vo.props); + } +} diff --git a/packages/core/src/domain/ValueObjectArray.ts b/packages/core/src/domain/ValueObjectArray.ts new file mode 100644 index 00000000..17f487a0 --- /dev/null +++ b/packages/core/src/domain/ValueObjectArray.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ValueObject } from "./ValueObject"; + +export class ValueObjectArray { + static inArray<VO extends ValueObject<any>>( + object: VO, + array: VO[] + ): boolean { + return array.some((o) => o.equals(object)); + } +} diff --git a/packages/core/src/domain/ValueObjectMap.ts b/packages/core/src/domain/ValueObjectMap.ts new file mode 100644 index 00000000..eb1f825a --- /dev/null +++ b/packages/core/src/domain/ValueObjectMap.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { ValueObject } from "./ValueObject"; + +export class ValueObjectMap<K extends ValueObject<any>, V> + implements Map<K, V> { + private readonly map: Map<K, V>; + + constructor(tuples?: [K, V][]) { + this.map = new Map<K, V>(tuples); + } + + private getKeyRef(key: K): K | undefined { + // eslint-disable-next-line no-restricted-syntax + for (const i of this.map.keys()) { + if (i.equals(key)) { + return i; + } + } + return undefined; + } + + clear(): void { + this.map.clear(); + } + + delete(key: K): boolean { + const keyRef = this.getKeyRef(key); + if (!keyRef) { + return false; + } + return this.delete(keyRef); + } + + forEach( + callbackfn: (value: V, key: K, map: Map<K, V>) => void, + thisArg?: any + ): void { + this.map.forEach(callbackfn, thisArg); + } + + get(key: K): V | undefined { + const keyRef = this.getKeyRef(key); + if (!keyRef) { + return undefined; + } + return this.map.get(keyRef); + } + + has(key: K): boolean { + const keyRef = this.getKeyRef(key); + if (!keyRef) { + return false; + } + return this.map.has(keyRef); + } + + set(key: K, value: V): this { + this.map.set(key, value); + return this; + } + + get size(): number { + return this.map.size; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.map[Symbol.iterator](); + } + + entries(): IterableIterator<[K, V]> { + return this.map.entries(); + } + + keys(): IterableIterator<K> { + return this.map.keys(); + } + + values(): IterableIterator<V> { + return this.map.values(); + } + + get [Symbol.toStringTag](): string { + return this.map[Symbol.toStringTag]; + } +} diff --git a/packages/core/src/domain/index.ts b/packages/core/src/domain/index.ts new file mode 100644 index 00000000..37864b4a --- /dev/null +++ b/packages/core/src/domain/index.ts @@ -0,0 +1,6 @@ +export * from "./AggregateRoot"; +export * from "./Entity"; +export * from "./Log4brainsError"; +export * from "./ValueObject"; +export * from "./ValueObjectArray"; +export * from "./ValueObjectMap"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..9a3c9fb9 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,6 @@ +import "./polyfills"; + +export * from "./infrastructure/api"; +export * from "./infrastructure/file-watcher"; +export { Log4brainsError } from "./domain"; +export { Log4brainsConfig } from "./infrastructure/config"; diff --git a/packages/core/src/infrastructure/api/Log4brains.ts b/packages/core/src/infrastructure/api/Log4brains.ts new file mode 100644 index 00000000..295c96d7 --- /dev/null +++ b/packages/core/src/infrastructure/api/Log4brains.ts @@ -0,0 +1,214 @@ +import { AwilixContainer } from "awilix"; +import { buildContainer } from "@src/infrastructure/di"; +import open from "open"; +import launchEditor from "launch-editor"; +import { Adr, AdrSlug, AdrStatus, PackageRef } from "@src/adr/domain"; +import { + CreateAdrFromTemplateCommand, + SupersedeAdrCommand +} from "@src/adr/application/commands"; +import { + SearchAdrsQuery, + SearchAdrsFilters as AppSearchAdrsFilters, + GenerateAdrSlugFromTitleQuery, + AdrRepository, + GetAdrBySlugQuery +} from "@src/adr/application"; +import { Log4brainsError } from "@src/domain"; +import { buildConfigFromWorkdir, Log4brainsConfig } from "../config"; +import { AdrDto, AdrDtoStatus } from "./types"; +import { adrToDto } from "./transformers"; +import { CommandBus, QueryBus } from "../buses"; +import { FileWatcher } from "../file-watcher"; + +export type SearchAdrsFilters = { + statuses?: AdrDtoStatus[]; +}; + +/** + * Log4brains core API. + * Use {@link Log4brains.create} to build an instance. + */ +export class Log4brains { + private readonly container: AwilixContainer; + + private readonly commandBus: CommandBus; + + private readonly queryBus: QueryBus; + + private readonly adrRepository: AdrRepository; + + private constructor( + public readonly config: Log4brainsConfig, + public readonly workdir = "." + ) { + this.container = buildContainer(config, workdir); + this.commandBus = this.container.resolve<CommandBus>("commandBus"); + this.queryBus = this.container.resolve<QueryBus>("queryBus"); + this.adrRepository = this.container.resolve<AdrRepository>("adrRepository"); + + // @see Adr.tz + Adr.setTz(config.project.tz); + } + + /** + * Returns the ADRs which match the given search filters. + * Returns all the ADRs of the project if no filter is given. + * The results are sorted with this order priority (ASC): + * 1. By the Date field from the markdown file (if available) + * 2. By the Git creation date (does not follow renames) + * 3. By slug + * + * @param filters Optional. Filters to apply to the search + * + * @throws {@link Log4brainsError} + * In case of a non-recoverable error. + */ + async searchAdrs(filters?: SearchAdrsFilters): Promise<AdrDto[]> { + const appFilters: AppSearchAdrsFilters = {}; + if (filters?.statuses) { + appFilters.statuses = filters.statuses.map((status) => + AdrStatus.createFromName(status) + ); + } + const adrs = await this.queryBus.dispatch<Adr[]>( + new SearchAdrsQuery(appFilters) + ); + return Promise.all( + adrs.map((adr) => adrToDto(adr, this.config.project.repository)) + ); + } + + /** + * Returns an ADR by its slug. + * + * @param slug ADR slug + * + * @throws {@link Log4brainsError} + * In case of a non-recoverable error. + */ + async getAdrBySlug(slug: string): Promise<AdrDto | undefined> { + const adr = await this.queryBus.dispatch<Adr | undefined>( + new GetAdrBySlugQuery(new AdrSlug(slug)) + ); + return adr ? adrToDto(adr, this.config.project.repository) : undefined; + } + + /** + * Generates an available ADR slug for the given title and package. + * Format: [package-name/]yyyymmdd-slugified-lowercased-title + * + * @param title The title of the ADR + * @param packageName Optional. The package name of the ADR. + * + * @throws {@link Log4brainsError} + * In case of a non-recoverable error. + */ + async generateAdrSlug(title: string, packageName?: string): Promise<string> { + const packageRef = packageName ? new PackageRef(packageName) : undefined; + return ( + await this.queryBus.dispatch<AdrSlug>( + new GenerateAdrSlugFromTitleQuery(title, packageRef) + ) + ).value; + } + + /** + * Creates a new ADR with the given slug and title with the default template. + * @param slug The slug of the ADR + * @param title The title of the ADR + * + * @throws {@link Log4brainsError} + * In case of a non-recoverable error. + */ + async createAdrFromTemplate(slug: string, title: string): Promise<AdrDto> { + const slugObj = new AdrSlug(slug); + await this.commandBus.dispatch( + new CreateAdrFromTemplateCommand(slugObj, title) + ); + return adrToDto(await this.adrRepository.find(slugObj)); + } + + /** + * Supersede an ADR with another one. + * @param supersededSlug Slug of the superseded ADR + * @param supersederSlug Slug of the superseder ADR + * + * @throws {@link Log4brainsError} + * In case of a non-recoverable error. + */ + async supersedeAdr( + supersededSlug: string, + supersederSlug: string + ): Promise<void> { + const supersededSlugObj = new AdrSlug(supersededSlug); + const supersederSlugObj = new AdrSlug(supersederSlug); + await this.commandBus.dispatch( + new SupersedeAdrCommand(supersededSlugObj, supersederSlugObj) + ); + } + + /** + * Opens the given ADR in an editor on the local machine. + * Tries first to guess the preferred editor of the user thanks to https://github.com/yyx990803/launch-editor. + * If impossible to guess, uses xdg-open (or similar, depending on the OS, thanks to https://github.com/sindresorhus/open) as a fallback. + * The overall order is thus the following: + * 1) The currently running editor among the supported ones by launch-editor + * 2) The editor defined by the $VISUAL environment variable + * 3) The editor defined by the $EDITOR environment variable + * 4) Fallback: xdg-open or similar + * + * @param slug The ADR slug to open + * @param onImpossibleToGuess Optional. Callback called when the fallback method is used. + * Useful to display a warning to the user to tell him to set his $VISUAL environment variable for the next time. + * + * @throws {@link Log4brainsError} + * If the ADR does not exist or if even the fallback method fails. + */ + async openAdrInEditor( + slug: string, + onImpossibleToGuess?: () => void + ): Promise<void> { + const adr = await this.queryBus.dispatch<Adr | undefined>( + new GetAdrBySlugQuery(new AdrSlug(slug)) + ); + if (!adr) { + throw new Log4brainsError("This ADR does not exist", slug); + } + const { file } = adr; + if (!file) { + throw new Log4brainsError( + "You are trying to open an non-saved ADR", + slug + ); + } + + launchEditor(file.path.absolutePath, undefined, async () => { + await open(file.path.absolutePath); + if (onImpossibleToGuess) { + onImpossibleToGuess(); + } + }); + } + + /** + * Returns a singleton instance of FileWatcher. + * Useful for Hot Reloading. + * @see FileWatcher + */ + get fileWatcher(): FileWatcher { + return this.container.resolve<FileWatcher>("fileWatcher"); + } + + /** + * Creates an instance of the Log4brains API. + * + * @param workdir Path to working directory (ie. where ".log4brains.yml" is located) + * + * @throws {@link Log4brainsError} + * In case of missing or invalid config file. + */ + static create(workdir = "."): Log4brains { + return new Log4brains(buildConfigFromWorkdir(workdir), workdir); + } +} diff --git a/packages/core/src/infrastructure/api/index.ts b/packages/core/src/infrastructure/api/index.ts new file mode 100644 index 00000000..de29dddb --- /dev/null +++ b/packages/core/src/infrastructure/api/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./Log4brains"; diff --git a/packages/core/src/infrastructure/api/transformers/adr-transformers.ts b/packages/core/src/infrastructure/api/transformers/adr-transformers.ts new file mode 100644 index 00000000..ae26b390 --- /dev/null +++ b/packages/core/src/infrastructure/api/transformers/adr-transformers.ts @@ -0,0 +1,60 @@ +import { Adr, AdrFile } from "@src/adr/domain"; +import { GitRepositoryConfig } from "@src/infrastructure/config"; +import { deepFreeze } from "@src/utils"; +import { AdrDto, AdrDtoStatus } from "../types"; + +function buildViewUrl( + repositoryConfig: GitRepositoryConfig, + file: AdrFile +): string | undefined { + if (!repositoryConfig.url || !repositoryConfig.viewFileUriPattern) { + return undefined; + } + const uri = repositoryConfig.viewFileUriPattern + .replace("%branch", "master") // TODO: make this customizable + .replace("%path", file.path.pathRelativeToCwd); + return `${repositoryConfig.url.replace(/\.git$/, "")}${uri}`; +} + +export async function adrToDto( + adr: Adr, + repositoryConfig?: GitRepositoryConfig +): Promise<AdrDto> { + if (!adr.file) { + throw new Error("You are serializing an non-saved ADR"); + } + + const viewUrl = repositoryConfig + ? buildViewUrl(repositoryConfig, adr.file) + : undefined; + + return deepFreeze<AdrDto>({ + slug: adr.slug.value, + package: adr.package?.name || null, + title: adr.title || null, + status: adr.status.name as AdrDtoStatus, + supersededBy: adr.superseder?.value || null, + tags: adr.tags, + deciders: adr.deciders, + body: { + rawMarkdown: adr.body.getRawMarkdown(), + enhancedMdx: await adr.getEnhancedMdx() + }, + creationDate: adr.creationDate.toJSON(), + lastEditDate: adr.lastEditDate.toJSON(), + lastEditAuthor: adr.lastEditAuthor.name, + publicationDate: adr.publicationDate?.toJSON() || null, + file: { + relativePath: adr.file.path.pathRelativeToCwd, + absolutePath: adr.file.path.absolutePath + }, + ...(repositoryConfig && repositoryConfig.provider && viewUrl + ? { + repository: { + provider: repositoryConfig.provider, + viewUrl + } + } + : undefined) + }); +} diff --git a/packages/core/src/infrastructure/api/transformers/index.ts b/packages/core/src/infrastructure/api/transformers/index.ts new file mode 100644 index 00000000..6bd15c51 --- /dev/null +++ b/packages/core/src/infrastructure/api/transformers/index.ts @@ -0,0 +1 @@ +export * from "./adr-transformers"; diff --git a/packages/core/src/infrastructure/api/types/AdrDto.ts b/packages/core/src/infrastructure/api/types/AdrDto.ts new file mode 100644 index 00000000..b9ba74a6 --- /dev/null +++ b/packages/core/src/infrastructure/api/types/AdrDto.ts @@ -0,0 +1,37 @@ +import { GitProvider } from "@src/infrastructure/config"; + +export type AdrDtoStatus = + | "draft" + | "proposed" + | "rejected" + | "accepted" + | "deprecated" + | "superseded"; + +// Dates are string (Date.toJSON()) because because Next.js cannot serialize Date objects + +export type AdrDto = Readonly<{ + slug: string; // Follows this pattern: <package name>/<sub slug> or just <sub slug> when the ADR does not belong to a specific package + package: string | null; // Null when the ADR does not belong to a package + title: string | null; + status: AdrDtoStatus; + supersededBy: string | null; // Optionally contains the target ADR slug when status === "superseded" + tags: string[]; // Can be empty + deciders: string[]; // Can be empty. In this case, it is encouraged to use lastEditAuthor as the only decider + body: Readonly<{ + rawMarkdown: string; + enhancedMdx: string; + }>; + creationDate: string; // Comes from Git or filesystem + lastEditDate: string; // Comes from Git or filesystem + lastEditAuthor: string; // Comes from Git (Git last author, or current Git user.name when unversioned, or "Anonymous" when Git is not installed) + publicationDate: string | null; // Comes from the Markdown body + file: Readonly<{ + relativePath: string; + absolutePath: string; + }>; + repository?: Readonly<{ + provider: GitProvider; + viewUrl: string; + }>; +}>; diff --git a/packages/core/src/infrastructure/api/types/index.ts b/packages/core/src/infrastructure/api/types/index.ts new file mode 100644 index 00000000..fc081cf0 --- /dev/null +++ b/packages/core/src/infrastructure/api/types/index.ts @@ -0,0 +1 @@ +export * from "./AdrDto"; diff --git a/packages/core/src/infrastructure/buses/CommandBus.ts b/packages/core/src/infrastructure/buses/CommandBus.ts new file mode 100644 index 00000000..985c78bf --- /dev/null +++ b/packages/core/src/infrastructure/buses/CommandBus.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { Command, CommandHandler } from "@src/application"; + +export class CommandBus { + private readonly handlersByCommandName: Map<string, CommandHandler> = new Map< + string, + CommandHandler + >(); + + registerHandler(handler: CommandHandler, commandClass: Function): void { + this.handlersByCommandName.set(commandClass.name, handler); + } + + async dispatch(command: Command): Promise<void> { + const commandName = command.constructor.name; + const handler = this.handlersByCommandName.get(commandName); + if (!handler) { + throw new Error(`No handler registered for this command: ${commandName}`); + } + return handler.execute(command); + } +} diff --git a/packages/core/src/infrastructure/buses/QueryBus.ts b/packages/core/src/infrastructure/buses/QueryBus.ts new file mode 100644 index 00000000..0ba3c8e9 --- /dev/null +++ b/packages/core/src/infrastructure/buses/QueryBus.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { Query, QueryHandler } from "@src/application"; + +export class QueryBus { + private readonly handlersByQueryName: Map<string, QueryHandler> = new Map< + string, + QueryHandler + >(); + + registerHandler(handler: QueryHandler, queryClass: Function): void { + this.handlersByQueryName.set(queryClass.name, handler); + } + + async dispatch<QR>(query: Query): Promise<QR> { + const queryName = query.constructor.name; + const handler = this.handlersByQueryName.get(queryName); + if (!handler) { + throw new Error(`No handler registered for this query: ${queryName}`); + } + return handler.execute(query) as Promise<QR>; + } +} diff --git a/packages/core/src/infrastructure/buses/index.ts b/packages/core/src/infrastructure/buses/index.ts new file mode 100644 index 00000000..f152a233 --- /dev/null +++ b/packages/core/src/infrastructure/buses/index.ts @@ -0,0 +1,2 @@ +export * from "./CommandBus"; +export * from "./QueryBus"; diff --git a/packages/core/src/infrastructure/config/builders.ts b/packages/core/src/infrastructure/config/builders.ts new file mode 100644 index 00000000..b6e32d2a --- /dev/null +++ b/packages/core/src/infrastructure/config/builders.ts @@ -0,0 +1,85 @@ +import path from "path"; +import fs from "fs"; +import yaml from "yaml"; +import { deepFreeze } from "@src/utils"; +import { Log4brainsError } from "@src/domain"; +import { Log4brainsConfig, schema } from "./schema"; +import { guessGitRepositoryConfig } from "./guessGitRepositoryConfig"; + +const configFilename = ".log4brains.yml"; + +function getDuplicatedValues<O extends Record<string, unknown>>( + objects: O[], + key: keyof O +): string[] { + const values = objects.map((object) => object[key]) as string[]; + const countsMap = values.reduce<Record<string, number>>((counts, value) => { + return { + ...counts, + [value]: (counts[value] || 0) + 1 + }; + }, {}); + return Object.keys(countsMap).filter((value) => countsMap[value] > 1); +} + +export function buildConfig(object: Record<string, unknown>): Log4brainsConfig { + const joiResult = schema.validate(object, { + abortEarly: false, + convert: false + }); + if (joiResult.error) { + throw new Log4brainsError( + `There is an error in the ${configFilename} config file`, + joiResult.error?.message + ); + } + const config = deepFreeze(joiResult.value) as Log4brainsConfig; + + // Package name duplication + if (config.project.packages) { + const duplicatedPackageNames = getDuplicatedValues( + config.project.packages, + "name" + ); + if (duplicatedPackageNames.length > 0) { + throw new Log4brainsError( + "Some package names are duplicated", + duplicatedPackageNames.join(", ") + ); + } + } + + return config; +} + +export function buildConfigFromWorkdir(workdir = "."): Log4brainsConfig { + const workdirAbsolute = path.resolve(workdir); + const configPath = path.join(workdirAbsolute, configFilename); + if (!fs.existsSync(configPath)) { + throw new Log4brainsError( + `Impossible to find the ${configFilename} config file`, + `workdir: ${workdirAbsolute}` + ); + } + + try { + const content = fs.readFileSync(configPath, "utf8"); + const object = yaml.parse(content) as Record<string, unknown>; + const config = buildConfig(object); + return deepFreeze({ + ...config, + project: { + ...config.project, + repository: guessGitRepositoryConfig(config, workdir) + } + }) as Log4brainsConfig; + } catch (e) { + if (e instanceof Log4brainsError) { + throw e; + } + throw new Log4brainsError( + `Impossible to read the ${configFilename} config file`, + e + ); + } +} diff --git a/packages/core/src/infrastructure/config/guessGitRepositoryConfig.ts b/packages/core/src/infrastructure/config/guessGitRepositoryConfig.ts new file mode 100644 index 00000000..f1dd0d88 --- /dev/null +++ b/packages/core/src/infrastructure/config/guessGitRepositoryConfig.ts @@ -0,0 +1,84 @@ +import gitUrlParse from "git-url-parse"; +import parseGitConfig from "parse-git-config"; +import path from "path"; +import { GitRepositoryConfig, Log4brainsConfig, gitProviders } from "./schema"; + +type GitRemoteConfig = { + url: string; +}; +function isGitRemoteConfig( + remoteConfig: unknown +): remoteConfig is GitRemoteConfig { + return ( + typeof remoteConfig === "object" && + remoteConfig !== null && + "url" in remoteConfig + ); +} + +export function guessGitRepositoryConfig( + existingConfig: Log4brainsConfig, + workdir: string +): GitRepositoryConfig | undefined { + // URL + let url = existingConfig.project.repository?.url; + if (!url) { + // Try to guess from the current Git configuration + // We use parse-git-config and not SimpleGit because we want this method to remain synchronous + const gitConfig = parseGitConfig.sync({ + path: path.join(workdir, ".git/config") + }); + if (isGitRemoteConfig(gitConfig['remote "origin"'])) { + url = gitConfig['remote "origin"'].url; + } + } + if (!url) { + return undefined; + } + + const urlInfo = gitUrlParse(url); + if ( + !urlInfo.protocol.includes("https") && + !urlInfo.protocol.includes("http") + ) { + // Probably an SSH URL -> we try to convert it to HTTPS + url = urlInfo.toString("https"); + } + + url = url.replace(/\/$/, ""); // remove a possible trailing-slash + + // PROVIDER + let provider = existingConfig.project.repository?.provider; + if (!provider || !gitProviders.includes(provider)) { + // Try to guess from the URL + provider = + gitProviders.filter((p) => urlInfo.resource.includes(p)).pop() || + "generic"; + } + + // PATTERN + let viewFileUriPattern = + existingConfig.project.repository?.viewFileUriPattern; + if (!viewFileUriPattern) { + switch (provider) { + case "gitlab": + viewFileUriPattern = "/-/blob/%branch/%path"; + break; + + case "bitbucket": + viewFileUriPattern = "/src/%branch/%path"; + break; + + case "github": + default: + viewFileUriPattern = "/blob/%branch/%path"; + break; + } + } + + return { + url, + provider, + viewFileUriPattern + }; +} diff --git a/packages/core/src/infrastructure/config/index.ts b/packages/core/src/infrastructure/config/index.ts new file mode 100644 index 00000000..6075eb32 --- /dev/null +++ b/packages/core/src/infrastructure/config/index.ts @@ -0,0 +1,2 @@ +export * from "./builders"; +export { Log4brainsConfig, GitProvider, GitRepositoryConfig } from "./schema"; diff --git a/packages/core/src/infrastructure/config/schema.ts b/packages/core/src/infrastructure/config/schema.ts new file mode 100644 index 00000000..4e85ecbd --- /dev/null +++ b/packages/core/src/infrastructure/config/schema.ts @@ -0,0 +1,58 @@ +import Joi from "joi"; + +type ProjectPackageConfig = Readonly<{ + name: string; + path: string; + adrFolder: string; +}>; + +const projectPackageSchema = Joi.object({ + name: Joi.string().hostname().required(), + path: Joi.string().required(), + adrFolder: Joi.string().required() +}); + +export const gitProviders = [ + "github", + "gitlab", + "bitbucket", + "generic" +] as const; +export type GitProvider = typeof gitProviders[number]; + +// Optional values are automatically guessed at configuration build time +export type GitRepositoryConfig = Readonly<{ + url?: string; + provider?: GitProvider; + viewFileUriPattern?: string; +}>; + +const gitRepositorySchema = Joi.object({ + url: Joi.string().uri(), // Guessed from the current Git configuration if omitted + provider: Joi.string().valid(...gitProviders), // Guessed from url if omitted (useful for enterprise plans with custom domains) + viewFileUriPattern: Joi.string() // Useful for unsupported providers. Example for GitHub: /blob/%branch/%path +}); + +type ProjectConfig = Readonly<{ + name: string; + tz: string; + adrFolder: string; + packages?: ProjectPackageConfig[]; + repository?: GitRepositoryConfig; +}>; + +const projectSchema = Joi.object({ + name: Joi.string().required(), + tz: Joi.string().required(), + adrFolder: Joi.string().required(), + packages: Joi.array().items(projectPackageSchema), + repository: gitRepositorySchema +}); + +export type Log4brainsConfig = Readonly<{ + project: ProjectConfig; +}>; + +export const schema = Joi.object({ + project: projectSchema.required() +}); diff --git a/packages/core/src/infrastructure/di/buildContainer.ts b/packages/core/src/infrastructure/di/buildContainer.ts new file mode 100644 index 00000000..41637ea6 --- /dev/null +++ b/packages/core/src/infrastructure/di/buildContainer.ts @@ -0,0 +1,89 @@ +import { + createContainer, + asValue, + InjectionMode, + asClass, + AwilixContainer, + asFunction +} from "awilix"; +import { Log4brainsConfig } from "@src/infrastructure/config"; +import * as adrCommandHandlers from "@src/adr/application/command-handlers"; +import * as adrQueryHandlers from "@src/adr/application/query-handlers"; +import { CommandHandler, QueryHandler } from "@src/application"; +import * as repositories from "@src/adr/infrastructure/repositories"; +import { CommandBus, QueryBus } from "../buses"; +import { FileWatcher } from "../file-watcher"; + +function lowerCaseFirstLetter(string: string): string { + return string.charAt(0).toLowerCase() + string.slice(1); +} + +export function buildContainer( + config: Log4brainsConfig, + workdir = "." +): AwilixContainer { + const container: AwilixContainer = createContainer({ + injectionMode: InjectionMode.PROXY + }); + + // Configuration & misc + container.register({ + config: asValue(config), + workdir: asValue(workdir), + fileWatcher: asClass(FileWatcher).singleton() + }); + + // Repositories + Object.values(repositories).forEach((Repository) => { + container.register( + lowerCaseFirstLetter(Repository.name), + asClass<unknown>(Repository).singleton() + ); + }); + + // Command handlers + Object.values(adrCommandHandlers).forEach((Handler) => { + container.register( + Handler.name, + asClass<CommandHandler>(Handler).singleton() + ); + }); + + // Command bus + container.register({ + commandBus: asFunction(() => { + const bus = new CommandBus(); + + Object.values(adrCommandHandlers).forEach((Handler) => { + const handlerInstance = container.resolve<CommandHandler>(Handler.name); + bus.registerHandler(handlerInstance, handlerInstance.commandClass); + }); + + return bus; + }).singleton() + }); + + // Query handlers + Object.values(adrQueryHandlers).forEach((Handler) => { + container.register( + Handler.name, + asClass<QueryHandler>(Handler).singleton() + ); + }); + + // Query bus + container.register({ + queryBus: asFunction(() => { + const bus = new QueryBus(); + + Object.values(adrQueryHandlers).forEach((Handler) => { + const handlerInstance = container.resolve<QueryHandler>(Handler.name); + bus.registerHandler(handlerInstance, handlerInstance.queryClass); + }); + + return bus; + }).singleton() + }); + + return container; +} diff --git a/packages/core/src/infrastructure/di/index.ts b/packages/core/src/infrastructure/di/index.ts new file mode 100644 index 00000000..94226c6f --- /dev/null +++ b/packages/core/src/infrastructure/di/index.ts @@ -0,0 +1 @@ +export * from "./buildContainer"; diff --git a/packages/core/src/infrastructure/file-watcher/FileWatcher.ts b/packages/core/src/infrastructure/file-watcher/FileWatcher.ts new file mode 100644 index 00000000..5ff9223c --- /dev/null +++ b/packages/core/src/infrastructure/file-watcher/FileWatcher.ts @@ -0,0 +1,83 @@ +import { Log4brainsError } from "@src/domain"; +import chokidar, { FSWatcher } from "chokidar"; +import { Log4brainsConfig } from "../config"; + +export type FileWatcherEventType = + | "add" + | "addDir" + | "change" + | "unlink" + | "unlinkDir"; + +export type FileWatcherEvent = { + type: FileWatcherEventType; + relativePath: string; +}; + +export type FileWatcherObserver = (event: FileWatcherEvent) => void; +export type FileWatcherUnsubscribeCb = () => void; + +type Deps = { + config: Log4brainsConfig; + workdir: string; +}; + +/** + * Watch files located in the main ADR folder, and in each package's ADR folder. + * Useful for Hot Reloading. + * The caller is responsible for starting and stopping it! + */ +export class FileWatcher { + private readonly config: Log4brainsConfig; + + private readonly workdir: string; + + private chokidar: FSWatcher | undefined; + + private observers: Set<FileWatcherObserver> = new Set<FileWatcherObserver>(); + + constructor({ config, workdir }: Deps) { + this.workdir = workdir; + this.config = config; + } + + subscribe(cb: FileWatcherObserver): FileWatcherUnsubscribeCb { + this.observers.add(cb); + return () => { + this.observers.delete(cb); + }; + } + + start(): void { + if (this.chokidar) { + throw new Log4brainsError("FileWatcher is already started"); + } + + const paths = [ + this.config.project.adrFolder, + ...(this.config.project.packages || []).map((pkg) => pkg.adrFolder) + ]; + this.chokidar = chokidar + .watch(paths, { + ignoreInitial: true, + cwd: this.workdir, + disableGlobbing: true + }) + .on("all", (event, filePath) => { + this.observers.forEach((observer) => + observer({ + type: event, + relativePath: filePath + }) + ); + }); + } + + async stop(): Promise<void> { + if (!this.chokidar) { + throw new Log4brainsError("FileWatcher is not started"); + } + await this.chokidar.close(); + this.chokidar = undefined; + } +} diff --git a/packages/core/src/infrastructure/file-watcher/index.ts b/packages/core/src/infrastructure/file-watcher/index.ts new file mode 100644 index 00000000..239014bf --- /dev/null +++ b/packages/core/src/infrastructure/file-watcher/index.ts @@ -0,0 +1 @@ +export * from "./FileWatcher"; diff --git a/packages/core/src/lib/cheerio-markdown/CheerioMarkdown.ts b/packages/core/src/lib/cheerio-markdown/CheerioMarkdown.ts new file mode 100644 index 00000000..ccaa60d8 --- /dev/null +++ b/packages/core/src/lib/cheerio-markdown/CheerioMarkdown.ts @@ -0,0 +1,141 @@ +import cheerio from "cheerio"; +import MarkdownIt from "markdown-it"; +import { markdownItSourceMap } from "./markdown-it-source-map-plugin"; +import { CheerioMarkdownElement } from "./CheerioMarkdownElement"; +import { cheerioToMarkdown } from "./cheerioToMarkdown"; + +// TODO: I am thinking to create a standalone library for this one + +const markdownItInstance = new MarkdownIt(); +markdownItInstance.use(markdownItSourceMap); + +function isWindowsLine(line: string): boolean { + return line.endsWith(`\r\n`) || line.endsWith(`\r`); +} + +type OnChangeObserver = (markdown: string) => void; + +export class CheerioMarkdown { + public $!: cheerio.Root; // for read-only purposes only! + + private readonly observers: OnChangeObserver[] = []; + + constructor(private $markdown: string) { + this.updateMarkdown($markdown); + } + + get markdown(): string { + return this.$markdown; + } + + get nbLines(): number { + return this.markdown.split(`\n`).length; + } + + onChange(cb: OnChangeObserver): void { + this.observers.push(cb); + } + + updateMarkdown(markdown: string): void { + this.$markdown = markdown; + this.$ = cheerio.load(markdownItInstance.render(this.markdown)); + this.observers.forEach((observer) => observer(this.markdown)); + } + + getLine(i: number): string { + const lines = this.markdown.split(/\r?\n/); + if (lines[i] === undefined) { + throw new Error(`Unknown line ${i}`); + } + return lines[i]; + } + + replaceText(elt: cheerio.Cheerio, newText: string): void { + const mdElt = new CheerioMarkdownElement(elt); + if (mdElt.startLine === undefined || mdElt.endLine === undefined) { + throw new Error("Cannot source-map this element from Markdown"); + } + + for (let i = mdElt.startLine; i < mdElt.endLine; i += 1) { + const newLine = this.getLine(mdElt.startLine).replace( + cheerioToMarkdown(elt), + newText + ); + this.replaceLine(mdElt.startLine, newLine); + } + } + + deleteElement(elt: cheerio.Cheerio): void { + const mdElt = new CheerioMarkdownElement(elt); + if (mdElt.startLine === undefined || mdElt.endLine === undefined) { + throw new Error("Cannot source-map this element from Markdown"); + } + this.deleteLines(mdElt.startLine, mdElt.endLine - 1); + } + + replaceLine(i: number, newLine: string): void { + const lines = this.markdown.split(`\n`); + if (lines[i] === undefined) { + throw new Error(`Unknown line ${i}`); + } + lines[i] = `${newLine}${isWindowsLine(lines[i]) ? `\r` : ""}`; + this.updateMarkdown(lines.join(`\n`)); + } + + deleteLines(start: number, end?: number): void { + const lines = this.markdown.split(`\n`); + if (lines[start] === undefined) { + throw new Error(`Unknown line ${start}`); + } + const length = end ? end - start + 1 : 1; + lines.splice(start, length); + this.updateMarkdown(lines.join(`\n`)); + } + + insertLineAt(i: number, newLine: string): void { + const lines = this.markdown.split(`\n`); + if (lines.length === 0) { + lines.push(`\n`); + } + if (lines[i] === undefined) { + throw new Error(`Unknown line ${i}`); + } + lines.splice(i, 0, `${newLine}${isWindowsLine(lines[i]) ? `\r` : ""}`); + this.updateMarkdown(lines.join(`\n`)); + } + + insertLineAfter(elt: cheerio.Cheerio, newLine: string): void { + const mdElt = new CheerioMarkdownElement(elt); + if (mdElt.endLine === undefined) { + throw new Error("Cannot source-map this element from Markdown"); + } + const end = elt.is("ul") ? mdElt.endLine - 1 : mdElt.endLine; + this.insertLineAt(end, newLine); + } + + appendLine(newLine: string): void { + const lines = this.markdown.split(`\n`); + const windowsLines = lines.length > 0 ? isWindowsLine(lines[0]) : false; + if (lines[lines.length - 1].trim() === "") { + delete lines[lines.length - 1]; + } + lines.push(`${newLine}${windowsLines ? `\r` : ""}`); + lines.push(`${windowsLines ? `\r` : ""}\n`); + this.updateMarkdown(lines.join(`\n`)); + } + + appendToList(ul: cheerio.Cheerio, newItem: string): void { + if (!ul.is("ul")) { + throw new TypeError("Given element is not a <ul>"); + } + const mdElt = new CheerioMarkdownElement(ul); + if (mdElt.markup === undefined || mdElt.level === undefined) { + throw new Error("Cannot source-map this element from Markdown"); + } + if (mdElt.level > 0) { + throw new Error("Sub-lists are not implemented yet"); + } + const newLine = `${mdElt.markup} ${newItem}`; + this.insertLineAfter(ul, newLine); + } +} diff --git a/packages/core/src/lib/cheerio-markdown/CheerioMarkdownElement.ts b/packages/core/src/lib/cheerio-markdown/CheerioMarkdownElement.ts new file mode 100644 index 00000000..5e22aaa5 --- /dev/null +++ b/packages/core/src/lib/cheerio-markdown/CheerioMarkdownElement.ts @@ -0,0 +1,25 @@ +import cheerio from "cheerio"; + +export class CheerioMarkdownElement { + constructor(private readonly cheerioElt: cheerio.Cheerio) {} + + get startLine(): number | undefined { + const data = this.cheerioElt.data("sourceLineStart") as string | undefined; + return data !== undefined ? parseInt(data, 10) : undefined; + } + + get endLine(): number | undefined { + const data = this.cheerioElt.data("sourceLineEnd") as string | undefined; + return data !== undefined ? parseInt(data, 10) : undefined; + } + + get markup(): string | undefined { + const data = this.cheerioElt.data("sourceMarkup") as string | undefined; + return data !== undefined ? data : undefined; + } + + get level(): number | undefined { + const data = this.cheerioElt.data("sourceLevel") as string | undefined; + return data !== undefined ? parseInt(data, 10) : undefined; + } +} diff --git a/packages/core/src/lib/cheerio-markdown/cheerioToMarkdown.ts b/packages/core/src/lib/cheerio-markdown/cheerioToMarkdown.ts new file mode 100644 index 00000000..166a8e41 --- /dev/null +++ b/packages/core/src/lib/cheerio-markdown/cheerioToMarkdown.ts @@ -0,0 +1,22 @@ +import cheerio from "cheerio"; + +export function cheerioToMarkdown( + elt: cheerio.Cheerio, + keepLinks = true +): string { + const html = elt.html(); + if (!html) { + return ""; + } + const copy = cheerio.load(html); + + if (keepLinks) { + copy("a").each((i, linkElt) => { + copy(linkElt).text( + `[${copy(linkElt).text()}](${copy(linkElt).attr("href")})` + ); + }); + } + + return copy("body").text(); +} diff --git a/packages/core/src/lib/cheerio-markdown/index.ts b/packages/core/src/lib/cheerio-markdown/index.ts new file mode 100644 index 00000000..88d477cf --- /dev/null +++ b/packages/core/src/lib/cheerio-markdown/index.ts @@ -0,0 +1,2 @@ +export * from "./CheerioMarkdown"; +export * from "./cheerioToMarkdown"; diff --git a/packages/core/src/lib/cheerio-markdown/markdown-it-source-map-plugin.ts b/packages/core/src/lib/cheerio-markdown/markdown-it-source-map-plugin.ts new file mode 100644 index 00000000..650d6d98 --- /dev/null +++ b/packages/core/src/lib/cheerio-markdown/markdown-it-source-map-plugin.ts @@ -0,0 +1,27 @@ +/* eslint-disable func-names */ +/* eslint-disable no-param-reassign */ +import MarkdownIt from "markdown-it"; + +// Source: https://github.com/tylingsoft/markdown-it-source-map +// Thanks! ;) +// Had to fork it to add additional information + +export function markdownItSourceMap(md: MarkdownIt): void { + const defaultRenderToken = md.renderer.renderToken.bind(md.renderer); + md.renderer.renderToken = function (tokens, idx, options) { + const token = tokens[idx]; + if (token.type.endsWith("_open")) { + if (token.map) { + token.attrPush(["data-source-line-start", token.map[0].toString()]); + token.attrPush(["data-source-line-end", token.map[1].toString()]); + } + if (token.markup !== undefined) { + token.attrPush(["data-source-markup", token.markup]); + } + if (token.level !== undefined) { + token.attrPush(["data-source-level", token.level.toString()]); + } + } + return defaultRenderToken(tokens, idx, options); + }; +} diff --git a/packages/core/src/lib/paths.ts b/packages/core/src/lib/paths.ts new file mode 100644 index 00000000..2ea7dbcb --- /dev/null +++ b/packages/core/src/lib/paths.ts @@ -0,0 +1,3 @@ +export function forceUnixPath(p: string): string { + return p.replace(/\\/g, "/"); +} diff --git a/packages/core/src/polyfills.ts b/packages/core/src/polyfills.ts new file mode 100644 index 00000000..a96f1b88 --- /dev/null +++ b/packages/core/src/polyfills.ts @@ -0,0 +1 @@ +import "core-js/features/array/flat"; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts new file mode 100644 index 00000000..0fef3336 --- /dev/null +++ b/packages/core/src/utils.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +const deepFreezeRecur = (obj: any): any => { + if (typeof obj !== "object") { + return obj; + } + Object.keys(obj).forEach((prop) => { + if (typeof obj[prop] === "object" && !Object.isFrozen(obj[prop])) { + deepFreezeRecur(obj[prop]); + } + }); + return Object.freeze(obj); +}; + +/** + * Apply Object.freeze() recursively on the given object and sub-objects. + */ +export const deepFreeze = <T>(obj: T): T => { + return deepFreezeRecur(obj); +}; diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json new file mode 100644 index 00000000..215794bb --- /dev/null +++ b/packages/core/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "integration-tests", "**/*.test.ts"] +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..bcfc6b06 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["ES2019.Array"], + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@src/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "integration-tests/**/*.ts"], + "exclude": ["node_modules"], + "ts-node": { "files": true } +} diff --git a/packages/init/.eslintrc.js b/packages/init/.eslintrc.js new file mode 100644 index 00000000..1f6675ff --- /dev/null +++ b/packages/init/.eslintrc.js @@ -0,0 +1,11 @@ +const path = require("path"); + +module.exports = { + env: { + node: true + }, + parserOptions: { + project: path.join(__dirname, "tsconfig.json") + }, + extends: ["../../.eslintrc"] +}; diff --git a/packages/init/README.md b/packages/init/README.md new file mode 100644 index 00000000..dc353000 --- /dev/null +++ b/packages/init/README.md @@ -0,0 +1,24 @@ +# init-log4brains + +This interactive CLI lets you install and configure the [Log4brains](https://github.com/thomvaill/log4brains) architecture knowledge base in your project. + +## Usage + +You should have a look at the main [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md) to get more context. + +Start the interactive CLI by running this command in your project root directory: + +```bash +npx init-log4brains +``` + +It will: + +- Install `@log4brains/cli` and `@log4brains/web` as development dependencies in your project (it detects automatically whether you use npm or yarn) +- Add some entries in your `package.json`'s scripts: `adr`, `log4brains-preview`, `log4brains-build` +- Prompt you some questions in order to create the `.log4brains.yml` config file for you +- Import your existing Architecture Decision Records (ADR), or create a first one if you don't have any yet + +## Documentation + +- [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md) diff --git a/packages/init/assets/README.md b/packages/init/assets/README.md new file mode 100644 index 00000000..8e229b20 --- /dev/null +++ b/packages/init/assets/README.md @@ -0,0 +1,33 @@ +# Architecture Decision Records + +ADRs are automatically published to our Log4brains architecture knowledge base: + +🔗 **<http://INSERT-YOUR-LOG4BRAINS-URL>** + +Please use this link to browse them. + +## Development + +To preview the knowledge base locally, run: + +```bash +npm run log4brains-preview +# OR +yarn log4brains-preview +``` + +In preview mode, the Hot Reload feature is enabled: any change you make to a markdown file is applied live in the UI. + +To create a new ADR interactively, run: + +```bash +npm run adr new +# OR +yarn adr new +``` + +## More information + +- [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/master#readme) +- [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/master#-what-is-an-adr-and-why-should-you-use-them) +- [ADR GitHub organization](https://adr.github.io/) diff --git a/packages/init/assets/index.md b/packages/init/assets/index.md new file mode 100644 index 00000000..7b5e225f --- /dev/null +++ b/packages/init/assets/index.md @@ -0,0 +1,36 @@ +<!-- This file is the homepage of your Log4brains knowledge base. You are free to edit it as you want --> + +# Architecture knowledge base + +Welcome 👋 to the architecture knowledge base of {PROJECT_NAME}. +You will find here all the Architecture Decision Records (ADR) of the project. + +## Definition and purpose + +> An Architectural Decision (AD) is a software design choice that addresses a functional or non-functional requirement that is architecturally significant. +> An Architectural Decision Record (ADR) captures a single AD, such as often done when writing personal notes or meeting minutes; the collection of ADRs created and maintained in a project constitutes its decision log. + +An ADR is immutable: only its status can change (i.e., become deprecated or superseded). That way, you can become familiar with the whole project history just by reading its decision log in chronological order. +Moreover, maintaining this documentation aims at: + +- 🚀 Improving and speeding up the onboarding of a new team member +- 🔭 Avoiding blind acceptance/reversal of a past decision (cf [Michael Nygard's famous article on ADRs](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions.html)) +- 🤝 Formalizing the decision process of the team + +## Usage + +This website is automatically updated after a change on the `master` branch of the project's Git repository. +In fact, the developers manage this documentation directly with markdown files located next to their code, so it is more convenient for them to keep it up-to-date. +You can browse the ADRs by using the left menu or the search bar. + +The typical workflow of an ADR is the following: + +![ADR workflow](/l4b-static/adr-workflow.png) + +The decision process is entirely collaborative and backed by pull requests. + +## More information + +- [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/master#readme) +- [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/master#-what-is-an-adr-and-why-should-you-use-them) +- [ADR GitHub organization](https://adr.github.io/) diff --git a/packages/init/assets/template.md b/packages/init/assets/template.md new file mode 100644 index 00000000..35479fbc --- /dev/null +++ b/packages/init/assets/template.md @@ -0,0 +1,73 @@ +# [short title of solved problem and solution] + +- Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [xxx](yyyymmdd-xxx.md)] <!-- optional --> +- Deciders: [list everyone involved in the decision] <!-- optional --> +- Date: [YYYY-MM-DD when the decision was last updated] <!-- optional. To customize the ordering without relying on last edit dates --> +- Tags: [space and/or comma separated list of tags] <!-- optional --> + +Technical Story: [description | ticket/issue URL] <!-- optional --> + +## Context and Problem Statement + +[Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] + +## Decision Drivers <!-- optional --> + +- [driver 1, e.g., a force, facing concern, …] +- [driver 2, e.g., a force, facing concern, …] +- … <!-- numbers of drivers can vary --> + +## Considered Options + +- [option 1] +- [option 2] +- [option 3] +- … <!-- numbers of options can vary --> + +## Decision Outcome + +Chosen option: "[option 1]", because [justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force force | … | comes out best (see below)]. + +### Positive Consequences <!-- optional --> + +- [e.g., improvement of quality attribute satisfaction, follow-up decisions required, …] +- … + +### Negative Consequences <!-- optional --> + +- [e.g., compromising quality attribute, follow-up decisions required, …] +- … + +## Pros and Cons of the Options <!-- optional --> + +### [option 1] + +[example | description | pointer to more information | …] <!-- optional --> + +- Good, because [argument a] +- Good, because [argument b] +- Bad, because [argument c] +- … <!-- numbers of pros and cons can vary --> + +### [option 2] + +[example | description | pointer to more information | …] <!-- optional --> + +- Good, because [argument a] +- Good, because [argument b] +- Bad, because [argument c] +- … <!-- numbers of pros and cons can vary --> + +### [option 3] + +[example | description | pointer to more information | …] <!-- optional --> + +- Good, because [argument a] +- Good, because [argument b] +- Bad, because [argument c] +- … <!-- numbers of pros and cons can vary --> + +## Links <!-- optional --> + +- [Link type][link to adr] <!-- example: Refined by [xxx](yyyymmdd-xxx.md) --> +- … <!-- numbers of links can vary --> diff --git a/packages/init/assets/use-log4brains-to-manage-the-adrs.md b/packages/init/assets/use-log4brains-to-manage-the-adrs.md new file mode 100644 index 00000000..41b40be7 --- /dev/null +++ b/packages/init/assets/use-log4brains-to-manage-the-adrs.md @@ -0,0 +1,21 @@ +# Use Log4brains to manage the ADRs + +- Status: accepted +- Date: {DATE} + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which tool(s) should we use to manage these records? + +## Considered Options + +- [Log4brains](https://github.com/thomvaill/log4brains): architecture knowledge base (command-line + static site generator) +- [ADR Tools](https://github.com/npryce/adr-tools): command-line to create ADRs +- [ADR Tools Python](https://bitbucket.org/tinkerer_/adr-tools-python/src/master/): command-line to create ADRs +- [adr-viewer](https://github.com/mrwilson/adr-viewer): static site generator +- [adr-log](https://adr.github.io/adr-log/): command-line to create a TOC of ADRs + +## Decision Outcome + +Chosen option: "Log4brains", because it includes the features of all the other tools, and even more. diff --git a/packages/init/assets/use-markdown-architectural-decision-records.md b/packages/init/assets/use-markdown-architectural-decision-records.md new file mode 100644 index 00000000..af462a4d --- /dev/null +++ b/packages/init/assets/use-markdown-architectural-decision-records.md @@ -0,0 +1,41 @@ +# Use Markdown Architectural Decision Records + +- Status: accepted +- Date: {DATE} + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which format and structure should these records follow? + +## Considered Options + +- [MADR](https://adr.github.io/madr/) 2.1.2 with Log4brains patch +- [MADR](https://adr.github.io/madr/) 2.1.2 – The original Markdown Architectural Decision Records +- [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +- [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements +- Other templates listed at <https://github.com/joelparkerhenderson/architecture_decision_record> +- Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 2.1.2 with Log4brains patch", because + +- Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +- The MADR format is lean and fits our development style. +- The MADR structure is comprehensible and facilitates usage & maintenance. +- The MADR project is vivid. +- Version 2.1.2 is the latest one available when starting to document ADRs. +- The Log4brains patch adds more features, like tags. + +The "Log4brains patch" performs the following modifications to the original template: + +- Change the ADR filenames format (`NNN-adr-name` becomes `YYYYMMDD-adr-name`), to avoid conflicts during Git merges. +- Add a `draft` status, to enable collaborative writing. +- Add a `Tags` field. + +## Links + +- Relates to [Use Log4brains to manage the ADRs]({LOG4BRAINS_ADR_SLUG}.md) diff --git a/packages/init/integration-tests/init.test.ts b/packages/init/integration-tests/init.test.ts new file mode 100644 index 00000000..5388e2ec --- /dev/null +++ b/packages/init/integration-tests/init.test.ts @@ -0,0 +1,203 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable global-require */ +/* eslint-disable @typescript-eslint/prefer-regexp-exec */ +/* eslint-disable jest/no-conditional-expect */ +/* eslint-disable jest/no-try-expect */ +import execa, { ExecaError } from "execa"; +import path from "path"; +import os from "os"; +import fs, { promises as fsP } from "fs"; +import rimraf from "rimraf"; + +type PackageJson = Record<string, unknown> & { + scripts: Record<string, string>; +}; + +// Source: https://shift.infinite.red/integration-testing-interactive-clis-93af3cc0d56f. Thanks! +const keys = { + up: "\x1B\x5B\x41", + down: "\x1B\x5B\x42", + enter: "\x0D", + space: "\x20" +}; + +// Inspired by Next.js's test/integration/create-next-app/index.test.js. Thank you! +const cliPath = path.join(__dirname, "../src/main"); +const run = (cwd: string) => + execa("node", ["-r", "esm", "-r", "ts-node/register", cliPath, cwd]); + +jest.setTimeout(1000 * 60); + +async function usingTempDir(fn: (cwd: string) => void | Promise<void>) { + const folder = await fsP.mkdtemp( + path.join(os.tmpdir(), "log4brains-init-tests-") + ); + try { + return await fn(folder); + } finally { + rimraf.sync(folder); + } +} + +type PackageAnswer = { + name: string; + path: string; + adrFolder: string; +}; +// eslint-disable-next-line sonarjs/cognitive-complexity +function bindAnswers( + cli: execa.ExecaChildProcess<string>, + packageAnswer?: PackageAnswer +): execa.ExecaChildProcess<string> { + if (!cli.stdout) { + throw new Error("CLI must have an stdout"); + } + + let name = false; + let type = false; + let adrFolder = false; + let packageName = false; + let packagePath = false; + let packageAdrFolder = false; + let packageDone = false; + cli.stdout.on("data", (data: Buffer) => { + const line = data.toString(); + if (!cli.stdin) { + throw new Error("CLI must have an stdin"); + } + + if (!name && line.match(/What is the name of your project\?/)) { + cli.stdin.write("\n"); + name = true; + } + if ( + !type && + line.match(/Which statement describes the best your project\?/) + ) { + if (packageAnswer) { + cli.stdin.write(keys.down); + } + cli.stdin.write("\n"); + type = true; + } + if ( + !adrFolder && + line.match( + /In which directory do you plan to store your( global)? ADRs\?/ + ) + ) { + cli.stdin.write("\n"); + adrFolder = true; + } + + // Multi only: + if (packageAnswer) { + if (!packageName && line.match(/Name\?/)) { + cli.stdin.write(`${packageAnswer.name}\n`); + packageName = true; + } + if ( + !packagePath && + line.match(/Where is located the source code of this package\?/) + ) { + cli.stdin.write(`${packageAnswer.path}\n`); + packagePath = true; + } + if ( + !packageAdrFolder && + line.match( + /In which directory do you plan to store the ADRs of this package\?/ + ) + ) { + cli.stdin.write(`${packageAnswer.adrFolder}\n`); + packageAdrFolder = true; + } + if (!packageDone && line.match(/Do you want to add another one\?/)) { + cli.stdin.write(`N\n`); + packageDone = true; + } + } + }); + return cli; +} + +describe("Init", () => { + test("empty directory", async () => { + await usingTempDir(async (cwd) => { + expect.assertions(1); + + try { + await run(cwd); + } catch (e) { + expect((e as ExecaError).stderr).toMatch( + /Impossible to find package\.json/ + ); + } + }); + }); + + test("fresh NPM mono project", async () => { + await usingTempDir(async (cwd) => { + await execa("npm", ["init", "--yes"], { cwd }); + await execa("npm", ["install"], { cwd }); + + await bindAnswers(run(cwd)); + + const pkgJson = require(path.join(cwd, "package.json")) as PackageJson; + expect(pkgJson.scripts.adr).toEqual("log4brains adr"); + expect(pkgJson.scripts["log4brains-preview"]).toEqual( + "log4brains-web preview" + ); + expect(pkgJson.scripts["log4brains-build"]).toEqual( + "log4brains-web build" + ); + + expect(fs.existsSync(path.join(cwd, ".log4brains.yml"))).toBeTruthy(); // TODO: test its content + expect(fs.existsSync(path.join(cwd, "docs/adr"))).toBeTruthy(); + expect( + fs.existsSync(path.join(cwd, "docs/adr/template.md")) + ).toBeTruthy(); + expect(fs.existsSync(path.join(cwd, "docs/adr/index.md"))).toBeTruthy(); + expect(fs.existsSync(path.join(cwd, "docs/adr/README.md"))).toBeTruthy(); + }); + }); + + test("fresh yarn mono project", async () => { + await usingTempDir(async (cwd) => { + await execa("npm", ["init", "--yes"], { cwd }); + await execa("yarn", ["install"], { cwd }); + + await bindAnswers(run(cwd)); + + const pkgJson = require(path.join(cwd, "package.json")) as PackageJson; + expect(pkgJson.scripts.adr).toEqual("log4brains adr"); + expect(pkgJson.scripts["log4brains-preview"]).toEqual( + "log4brains-web preview" + ); + expect(pkgJson.scripts["log4brains-build"]).toEqual( + "log4brains-web build" + ); + }); + }); + + test("fresh NPM multi project", async () => { + await usingTempDir(async (cwd) => { + await execa("npm", ["init", "--yes"], { cwd }); + await execa("npm", ["install"], { cwd }); + await execa("mkdir", ["-p", "packages/package1"], { cwd }); + + await bindAnswers(run(cwd), { + name: "package1", + path: "packages/package1", + adrFolder: "packages/package1/docs/adr" + }); + + expect(fs.existsSync(path.join(cwd, ".log4brains.yml"))).toBeTruthy(); // TODO: test its content + expect(fs.existsSync(path.join(cwd, "docs/adr"))).toBeTruthy(); + expect( + fs.existsSync(path.join(cwd, "packages/package1/docs/adr")) + ).toBeTruthy(); + }); + }); +}); diff --git a/packages/init/jest.config.js b/packages/init/jest.config.js new file mode 100644 index 00000000..03224ca2 --- /dev/null +++ b/packages/init/jest.config.js @@ -0,0 +1,13 @@ +const base = require("../../jest.config.base"); +const { pathsToModuleNameMapper } = require("ts-jest/utils"); +const { compilerOptions } = require("./tsconfig"); +const packageJson = require("./package"); + +module.exports = { + ...base, + name: packageJson.name, + displayName: packageJson.name, + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { + prefix: "<rootDir>/" + }) +}; diff --git a/packages/init/nodemon.json b/packages/init/nodemon.json new file mode 100644 index 00000000..e9329fd9 --- /dev/null +++ b/packages/init/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["src"], + "ext": "ts", + "exec": "yarn build" +} diff --git a/packages/init/package.json b/packages/init/package.json new file mode 100644 index 00000000..3e1b31e5 --- /dev/null +++ b/packages/init/package.json @@ -0,0 +1,67 @@ +{ + "name": "init-log4brains", + "version": "1.0.0-beta.0", + "description": "Install and configure the Log4brains architecture knowledge base in your project", + "keywords": [ + "log4brains", + "architecture decision records", + "architecture", + "knowledge base", + "documentation", + "docs-as-code", + "markdown", + "static site generator", + "documentation generator", + "tooling" + ], + "author": "Thomas Vaillant <thomvaill@bluebricks.dev>", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/thomvaill/log4brains", + "repository": { + "type": "git", + "url": "https://github.com/thomvaill/log4brains", + "directory": "packages/init" + }, + "engines": { + "node": ">=10.23.0" + }, + "files": [ + "assets", + "dist" + ], + "bin": "./dist/log4brains-init", + "scripts": { + "dev": "nodemon", + "build": "tsc --build tsconfig.build.json && copyfiles -u 1 src/log4brains-init dist", + "clean": "rimraf ./dist", + "typescript": "tsc --noEmit", + "test": "jest", + "test-watch": "jest --watch", + "lint": "eslint . --max-warnings=0", + "prepublishOnly": "yarn build", + "link": "npm link @log4brains/cli-common && npm link && rm -f ./package-lock.json" + }, + "dependencies": { + "@log4brains/cli-common": "^1.0.0-beta.0", + "chalk": "^4.1.0", + "commander": "^6.1.0", + "edit-json-file": "^1.5.0", + "esm": "^3.2.25", + "execa": "^4.1.0", + "has-yarn": "^2.1.0", + "mkdirp": "^1.0.4", + "moment-timezone": "^0.5.32", + "terminal-link": "^2.1.1", + "yaml": "^1.10.0" + }, + "devDependencies": { + "@types/edit-json-file": "^1.4.0", + "copyfiles": "^2.4.0", + "esm": "^3.2.25", + "ts-node": "^9.0.0" + } +} diff --git a/packages/init/src/cli.ts b/packages/init/src/cli.ts new file mode 100644 index 00000000..2a219eee --- /dev/null +++ b/packages/init/src/cli.ts @@ -0,0 +1,32 @@ +import commander from "commander"; +import type { AppConsole } from "@log4brains/cli-common"; +import { InitCommand, InitCommandOpts } from "./commands"; + +type Deps = { + appConsole: AppConsole; + version: string; + name: string; +}; + +export function createCli({ + appConsole, + name, + version +}: Deps): commander.Command { + return new commander.Command(name) + .version(version) + .arguments("[path]") + .description("Installs and configures Log4brains for your project", { + path: "Path of your project. Default: current directory" + }) + .option( + "-d, --defaults", + "Run in non-interactive mode and use the common default options", + false + ) + .action( + (path: string | undefined, options: InitCommandOpts): Promise<void> => { + return new InitCommand({ appConsole }).execute(options, path); + } + ); +} diff --git a/packages/init/src/commands/FailureExit.ts b/packages/init/src/commands/FailureExit.ts new file mode 100644 index 00000000..235b4894 --- /dev/null +++ b/packages/init/src/commands/FailureExit.ts @@ -0,0 +1,5 @@ +export class FailureExit extends Error { + constructor() { + super("The CLI exited with an error"); + } +} diff --git a/packages/init/src/commands/InitCommand.ts b/packages/init/src/commands/InitCommand.ts new file mode 100644 index 00000000..657ac1c3 --- /dev/null +++ b/packages/init/src/commands/InitCommand.ts @@ -0,0 +1,483 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable class-methods-use-this */ +import fs, { promises as fsP } from "fs"; +import terminalLink from "terminal-link"; +import chalk from "chalk"; +import hasYarn from "has-yarn"; +import execa from "execa"; +import mkdirp from "mkdirp"; +import yaml from "yaml"; +import path from "path"; +import editJsonFile from "edit-json-file"; +import moment from "moment-timezone"; +import type { AppConsole } from "@log4brains/cli-common"; +import { FailureExit } from "./FailureExit"; + +const assetsPath = path.resolve(path.join(__dirname, "../../assets")); +const docLink = "https://github.com/thomvaill/log4brains"; +const cliBinPath = "@log4brains/cli/dist/log4brains"; +const webBinPath = "@log4brains/web/dist/bin/log4brains-web"; + +function forceUnixPath(p: string): string { + return p.replace(/\\/g, "/"); +} + +export type InitCommandOpts = { + defaults: boolean; +}; + +type L4bYmlPackageConfig = { + name: string; + path: string; + adrFolder: string; +}; +type L4bYmlConfig = { + project: { + name: string; + tz: string; + adrFolder: string; + packages?: L4bYmlPackageConfig[]; + }; +}; + +type Deps = { + appConsole: AppConsole; +}; + +export class InitCommand { + private readonly console: AppConsole; + + private hasYarnValue?: boolean; + + constructor({ appConsole }: Deps) { + this.console = appConsole; + } + + private hasYarn(): boolean { + if (!this.hasYarnValue) { + this.hasYarnValue = hasYarn(); + } + return this.hasYarnValue; + } + + private isDev(): boolean { + return ( + process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" + ); + } + + private async installNpmPackages(cwd: string): Promise<void> { + const packages = ["@log4brains/cli", "@log4brains/web"]; + + if (this.isDev()) { + await execa("yarn", ["link", ...packages], { cwd }); + + // ... but unfortunately `yarn link` does not create the bin symlinks (https://github.com/yarnpkg/yarn/issues/5713) + // we have to do it ourselves: + await mkdirp(path.join(cwd, "node_modules/.bin")); + await execa( + "ln", + ["-s", "--force", `../${cliBinPath}`, "node_modules/.bin/log4brains"], + { cwd } + ); + await execa( + "ln", + [ + "-s", + "--force", + `../${webBinPath}`, + "node_modules/.bin/log4brains-web" + ], + { cwd } + ); + + this.console.println(); + this.console.println( + `${chalk.bgBlue.white.bold(" DEV ")} ${chalk.blue( + "Local packages are linked!" + )}` + ); + this.console.println(); + } else if (this.hasYarn()) { + await execa( + "yarn", + ["add", "--dev", "--ignore-workspace-root-check", ...packages], + { cwd } + ); + } else { + await execa("npm", ["install", "--save-dev", ...packages], { cwd }); + } + } + + private setupPackageJsonScripts(packageJsonPath: string): void { + const pkgJson = editJsonFile(packageJsonPath); + pkgJson.set("scripts.adr", "log4brains adr"); + pkgJson.set("scripts.log4brains-preview", "log4brains-web preview"); + pkgJson.set("scripts.log4brains-build", "log4brains-web build"); + pkgJson.save(); + } + + private guessMainAdrFolderPath(cwd: string): string | undefined { + const usualPaths = [ + "./docs/adr", + "./docs/adrs", + "./docs/architecture-decisions", + "./doc/adr", + "./doc/adrs", + "./doc/architecture-decisions", + "./adr", + "./adrs", + "./architecture-decisions" + ]; + // eslint-disable-next-line no-restricted-syntax + for (const possiblePath of usualPaths) { + if (fs.existsSync(path.join(cwd, possiblePath))) { + return possiblePath; + } + } + return undefined; + } + + // eslint-disable-next-line sonarjs/cognitive-complexity + private async buildLog4brainsConfigInteractively( + cwd: string, + noInteraction: boolean + ): Promise<L4bYmlConfig> { + this.console.println( + `We will now help you to create your ${chalk.cyan(".log4brains.yml")}...` + ); + this.console.println(); + + // Name + let name; + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,global-require,import/no-dynamic-require,@typescript-eslint/no-var-requires + name = require(path.join(cwd, "package.json")).name as string; + if (!name) { + throw Error("Empty name"); + } + } catch (e) { + this.console.warn( + `Impossible to get the project name from your ${chalk.cyan( + "package.json" + )}` + ); + } + name = noInteraction + ? name || "untitled" + : await this.console.askInputQuestion( + "What is the name of your project?", + name + ); + + // Project type + const type = noInteraction + ? "mono" + : await this.console.askListQuestion( + "Which statement describes the best your project?", + [ + { + name: "Simple project (only one ADR folder)", + value: "mono", + short: "Mono-package project" + }, + { + name: + "Multi-package project (one ADR folder per package + a global one)", + value: "multi", + short: "Multi-package project" + } + ] + ); + + // Main ADR folder location + let adrFolder = this.guessMainAdrFolderPath(cwd); + if (adrFolder) { + this.console.println( + `We have detected a possible existing ADR folder: ${chalk.cyan( + adrFolder + )}` + ); + adrFolder = + noInteraction || + (await this.console.askYesNoQuestion("Do you confirm?", true)) + ? adrFolder + : undefined; + } + if (!adrFolder) { + adrFolder = noInteraction + ? "./docs/adr" + : await this.console.askInputQuestion( + `In which directory do you plan to store your ${ + type === "multi" ? "global " : "" + }ADRs? (will be automatically created)`, + "./docs/adr" + ); + } + await mkdirp(path.join(cwd, adrFolder)); + this.console.println(); + + // Packages + const packages = []; + if (type === "multi") { + this.console.println("We will now define your packages..."); + this.console.println(); + + let oneMorePackage = false; + let packageNumber = 1; + do { + this.console.println(); + this.console.println( + ` ${chalk.underline(`Package #${packageNumber}`)}:` + ); + const pkgName = await this.console.askInputQuestion( + "Name? (short, lowercase, without special characters, nor spaces)" + ); + const pkgCodeFolder = await this.askPathWhileNotFound( + "Where is located the source code of this package?", + cwd, + `./packages/${pkgName}` + ); + const pkgAdrFolder = await this.console.askInputQuestion( + `In which directory do you plan to store the ADRs of this package? (will be automatically created)`, + `${pkgCodeFolder}/docs/adr` + ); + await mkdirp(path.join(cwd, pkgAdrFolder)); + packages.push({ + name: pkgName, + path: forceUnixPath(pkgCodeFolder), + adrFolder: forceUnixPath(pkgAdrFolder) + }); + oneMorePackage = await this.console.askYesNoQuestion( + `We are done with package #${packageNumber}. Do you want to add another one?`, + false + ); + packageNumber += 1; + } while (oneMorePackage); + } + + return { + project: { + name, + tz: moment.tz.guess(), + adrFolder: forceUnixPath(adrFolder), + packages + } + }; + } + + private async createAdr( + cwd: string, + adrFolder: string, + title: string, + source: string, + replacements: string[][] = [] + ): Promise<string> { + const slug = ( + await execa( + path.join(cwd, `node_modules/${cliBinPath}`), + [ + "adr", + "new", + "--quiet", + "--from", + forceUnixPath(path.join(assetsPath, source)), + `"${title}"` + ], + { cwd } + ) + ).stdout; + + // eslint-disable-next-line no-restricted-syntax + for (const replacement of [ + ["{DATE}", moment().format("YYYY-MM-DD")], + ...replacements + ]) { + await execa( + "sed", + [ + "-i", + `s/${replacement[0]}/${replacement[1]}/g`, + forceUnixPath(path.join(cwd, adrFolder, `${slug}.md`)) + ], + { + cwd + } + ); + } + + return slug; + } + + private async copyFileIfAbsent( + cwd: string, + adrFolder: string, + filename: string, + contentCb?: (content: string) => string + ): Promise<void> { + const outPath = path.join(cwd, adrFolder, filename); + if (!fs.existsSync(outPath)) { + let content = await fsP.readFile( + path.join(assetsPath, filename), + "utf-8" + ); + if (contentCb) { + content = contentCb(content); + } + await fsP.writeFile(outPath, content); + } + } + + private printSuccess(): void { + const runCmd = this.hasYarn() ? "yarn" : "npm run"; + const l4bCliCmdName = "adr"; + + this.console.success("Log4brains is installed and configured! 🎉🎉🎉"); + this.console.println(); + this.console.println("You can now use the CLI to create a new ADR:"); + this.console.println(` ${chalk.cyan(`${runCmd} ${l4bCliCmdName} new`)}`); + this.console.println(""); + this.console.println( + "And start the web UI to preview your architecture knowledge base:" + ); + this.console.println(` ${chalk.cyan(`${runCmd} log4brains-preview`)}`); + this.console.println(); + this.console.println( + "Do not forget to set up your CI/CD to automatically publish your knowledge base" + ); + this.console.println( + `Check out the ${terminalLink( + "documentation", + docLink + )} to see some examples` + ); + } + + private async askPathWhileNotFound( + question: string, + cwd: string, + defaultValue?: string + ): Promise<string> { + const p = await this.console.askInputQuestion(question, defaultValue); + if (!p.trim() || !fs.existsSync(path.join(cwd, p))) { + this.console.warn("This path does not exist. Please try again..."); + return this.askPathWhileNotFound(question, cwd, defaultValue); + } + return p; + } + + /** + * Command flow. + * + * @param options + * @param customCwd + */ + async execute(options: InitCommandOpts, customCwd?: string): Promise<void> { + const noInteraction = options.defaults; + + const cwd = customCwd ? path.resolve(customCwd) : process.cwd(); + if (!fs.existsSync(cwd)) { + this.console.fatal(`The given path does not exist: ${chalk.cyan(cwd)}`); + throw new FailureExit(); + } + + // Check package.json existence + const packageJsonPath = path.join(cwd, "package.json"); + if (!fs.existsSync(packageJsonPath)) { + this.console.fatal(`Impossible to find ${chalk.cyan("package.json")}`); + this.console.printlnErr( + "Are you sure to execute the command inside your project root directory?" + ); + this.console.printlnErr( + `Please refer to the ${terminalLink( + "documentation", + docLink + )} if you want to use Log4brains in a non-JS project or globally` + ); + throw new FailureExit(); + } + + // Install NPM packages + this.console.startSpinner("Install Log4brains packages..."); + await this.installNpmPackages(cwd); + this.console.stopSpinner(); + + // Setup package.json scripts + this.setupPackageJsonScripts(packageJsonPath); + this.console.println( + `We have added the following scripts to your ${chalk.cyan( + "package.json" + )}:` + ); + this.console.println(" - adr"); + this.console.println(" - log4brains-preview"); + this.console.println(" - log4brains-init"); + this.console.println(); + + // Terminate now if already configured + if (fs.existsSync(path.join(cwd, ".log4brains.yml"))) { + this.console.warn( + `${chalk.bold(".log4brains.yml")} already exists. We won't override it` + ); + this.console.warn( + "Please remove it and execute this command again if you want to configure it interactively" + ); + this.console.println(); + this.printSuccess(); + return; + } + + // Create .log4brains.yml interactively + const config = await this.buildLog4brainsConfigInteractively( + cwd, + noInteraction + ); + + this.console.startSpinner("Write config file..."); + const { adrFolder } = config.project; + await fsP.writeFile( + path.join(cwd, ".log4brains.yml"), + yaml.stringify(config), + "utf-8" + ); + + // Copy template, index and README if not already created + this.console.updateSpinner("Copy template files..."); + await this.copyFileIfAbsent(cwd, adrFolder, "template.md"); + await this.copyFileIfAbsent(cwd, adrFolder, "index.md", (content) => + content.replace(/{PROJECT_NAME}/g, config.project.name) + ); + await this.copyFileIfAbsent(cwd, adrFolder, "README.md"); + + // List existing ADRs + this.console.updateSpinner("Create your first ADR..."); + const adrListRes = await execa( + path.join(cwd, `node_modules/${cliBinPath}`), + ["adr", "list", "--raw"], + { cwd } + ); + + // Create Log4brains ADR + const l4bAdrSlug = await this.createAdr( + cwd, + adrFolder, + "Use Log4brains to manage the ADRs", + "use-log4brains-to-manage-the-adrs.md" + ); + + // Create MADR ADR if there was no ADR in the repository + if (!adrListRes.stdout) { + await this.createAdr( + cwd, + adrFolder, + "Use Markdown Architectural Decision Records", + "use-markdown-architectural-decision-records.md", + [["{LOG4BRAINS_ADR_SLUG}", l4bAdrSlug]] + ); + } + + // End + this.console.stopSpinner(); + this.printSuccess(); + } +} diff --git a/packages/init/src/commands/index.ts b/packages/init/src/commands/index.ts new file mode 100644 index 00000000..6de2c1e3 --- /dev/null +++ b/packages/init/src/commands/index.ts @@ -0,0 +1,2 @@ +export * from "./FailureExit"; +export * from "./InitCommand"; diff --git a/packages/init/src/log4brains-init b/packages/init/src/log4brains-init new file mode 100755 index 00000000..05abe630 --- /dev/null +++ b/packages/init/src/log4brains-init @@ -0,0 +1,5 @@ +#!/usr/bin/env node +require = require("esm")(module, { + mainFields: ["module", "main"] +}); +module.exports = require("./main"); diff --git a/packages/init/src/main.ts b/packages/init/src/main.ts new file mode 100644 index 00000000..1ef0ce72 --- /dev/null +++ b/packages/init/src/main.ts @@ -0,0 +1,29 @@ +import { AppConsole } from "@log4brains/cli-common"; +import { createCli } from "./cli"; +import { FailureExit } from "./commands"; + +const debug = !!process.env.DEBUG; +const dev = process.env.NODE_ENV === "development"; +const appConsole = new AppConsole({ debug, traces: debug || dev }); + +try { + // eslint-disable-next-line + const { name, version } = require("../package.json") as Record< + string, + string + >; + + const cli = createCli({ name, version, appConsole }); + cli.parseAsync(process.argv).catch((err) => { + if (!(err instanceof FailureExit)) { + if (appConsole.isSpinning()) { + appConsole.stopSpinner(true); + } + appConsole.fatal(err); + } + process.exit(1); + }); +} catch (e) { + appConsole.fatal(e); + process.exit(1); +} diff --git a/packages/init/tsconfig.build.json b/packages/init/tsconfig.build.json new file mode 100644 index 00000000..215794bb --- /dev/null +++ b/packages/init/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "integration-tests", "**/*.test.ts"] +} diff --git a/packages/init/tsconfig.json b/packages/init/tsconfig.json new file mode 100644 index 00000000..e98473ef --- /dev/null +++ b/packages/init/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@src/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "integration-tests/**/*.ts"], + "exclude": ["node_modules"], + "ts-node": { "files": true } +} diff --git a/packages/web/.babelrc b/packages/web/.babelrc new file mode 100644 index 00000000..e000594e --- /dev/null +++ b/packages/web/.babelrc @@ -0,0 +1,24 @@ +{ + "presets": ["next/babel"], + "plugins": [ + // Source: https://material-ui.com/guides/minimizing-bundle-size/ + [ + "babel-plugin-import", + { + "libraryName": "@material-ui/core", + "libraryDirectory": "", + "camel2DashComponentName": false + }, + "mui-core" + ], + [ + "babel-plugin-import", + { + "libraryName": "@material-ui/icons", + "libraryDirectory": "", + "camel2DashComponentName": false + }, + "mui-icons" + ] + ] +} diff --git a/packages/web/.eslintrc.js b/packages/web/.eslintrc.js new file mode 100644 index 00000000..f28fb5d2 --- /dev/null +++ b/packages/web/.eslintrc.js @@ -0,0 +1,23 @@ +const path = require("path"); + +module.exports = { + env: { + browser: true, + node: true + }, + parserOptions: { + ecmaFeatures: { + jsx: true + }, + project: path.join(__dirname, "tsconfig.dev.json") + }, + extends: ["../../.eslintrc"], + overrides: [ + { + files: ["src/pages/**/*.tsx", "src/pages/api/**/*.ts"], // Next.js pages and api routes + rules: { + "import/no-default-export": "off" + } + } + ] +}; diff --git a/packages/web/.gitignore b/packages/web/.gitignore new file mode 100644 index 00000000..1437c53f --- /dev/null +++ b/packages/web/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/packages/web/.storybook/main.js b/packages/web/.storybook/main.js new file mode 100644 index 00000000..20b31f2a --- /dev/null +++ b/packages/web/.storybook/main.js @@ -0,0 +1,22 @@ +const webpack = require("webpack"); + +module.exports = { + stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + addons: ["@storybook/addon-links", "@storybook/addon-essentials"], + + // Fix to make next/image work in Storybook (thanks https://stackoverflow.com/questions/64622746/how-to-mock-next-js-image-component-in-storybook) + webpackFinal: (config) => { + config.plugins.push( + new webpack.DefinePlugin({ + "process.env.__NEXT_IMAGE_OPTS": JSON.stringify({ + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + domains: [], + path: "/", + loader: "default" + }) + }) + ); + return config; + } +}; diff --git a/packages/web/.storybook/mocks/adrs.ts b/packages/web/.storybook/mocks/adrs.ts new file mode 100644 index 00000000..316dc6bc --- /dev/null +++ b/packages/web/.storybook/mocks/adrs.ts @@ -0,0 +1,204 @@ +import { AdrDtoStatus } from "@log4brains/core"; +import { Adr } from "../../src/types"; + +export const adrMocks = [ + { + slug: "20200101-use-markdown-architectural-decision-records", + package: null, + title: "Use Markdown Architectural Decision Records", + status: "accepted" as AdrDtoStatus, + supersededBy: null, + tags: [], + deciders: [], + body: { + enhancedMdx: ` +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which format and structure should these records follow? + +## Considered Options + +- [MADR](https://adr.github.io/madr/) 2.1.2 with log4brains patch +- [MADR](https://adr.github.io/madr/) 2.1.2 – The original Markdown Architectural Decision Records +- [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +- [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements +- Other templates listed at <https://github.com/joelparkerhenderson/architecture_decision_record> +- Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 2.1.2 with log4brains patch", because + +- Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +- The MADR format is lean and fits our development style. +- The MADR structure is comprehensible and facilitates usage & maintenance. +- The MADR project is vivid. +- Version 2.1.2 is the latest one available when starting to document ADRs. + +The "log4brains patch" performs the following modifications to the original template: + +- Add a draft status, to enable collaborative writing. +- Remove the Date field, because this metadata is already available in Git. +` + }, + creationDate: new Date(2020, 1, 1).toJSON(), + lastEditDate: new Date(2020, 10, 26).toJSON(), + lastEditAuthor: "John Doe", + publicationDate: new Date(2020, 1, 1).toJSON(), + repository: { + provider: "gitlab", + viewUrl: + "https://gitlab.com/foo/bar/-/blob/master/docs/adr/20200101-use-markdown-architectural-decision-records.md" + } + }, + { + slug: "frontend/20200102-use-nextjs-for-static-site-generation", + package: "frontend", + title: "Use Next.js for Static Site Generation", + status: "proposed" as AdrDtoStatus, + supersededBy: null, + tags: [], + deciders: [], + body: { enhancedMdx: "" }, + creationDate: new Date(2020, 1, 2).toJSON(), + lastEditDate: new Date(2020, 10, 26).toJSON(), + lastEditAuthor: "John Doe", + publicationDate: null, + repository: { + provider: "github", + viewUrl: + "https://github.com/foo/bar/blob/master/docs/adr/20200102-use-nextjs-for-static-site-generation.md" + } + }, + { + slug: "20200106-an-old-decision", + package: null, + title: "An old decision", + status: "superseded" as AdrDtoStatus, + supersededBy: "20200404-a-new-decision", + tags: [], + deciders: [], + body: { enhancedMdx: "Test" }, + creationDate: new Date(2020, 1, 6).toJSON(), + lastEditDate: new Date(2020, 1, 7).toJSON(), + lastEditAuthor: "John Doe", + publicationDate: new Date(2020, 1, 8).toJSON(), + repository: { + provider: "bitbucket", + viewUrl: + "https://bitbucket.org/foo/bar/src/master/docs/adr/20200106-an-old-decision.md" + } + }, + { + slug: "20200404-a-new-decision", + package: null, + title: "An new decision", + status: "accepted" as AdrDtoStatus, + supersededBy: null, + tags: [], + deciders: [], + body: { + enhancedMdx: `## Lorem Ipsum + +Ipsum Dolor + +<AdrLink slug="20200106-an-old-decision" status="superseded" title="An old decision" customLabel="This is a link with a custom label" /> + +## Links + +- Supersedes <AdrLink slug="20200106-an-old-decision" status="superseded" title="An old decision" /> +` + }, + creationDate: new Date(2020, 4, 4).toJSON(), + lastEditDate: new Date(2020, 10, 26).toJSON(), + lastEditAuthor: "John Doe", + publicationDate: null, + repository: { + provider: "github", + viewUrl: + "https://github.com/foo/bar/blob/master/docs/adr/20200404-a-new-decision.md" + } + }, + { + slug: "backend/20200404-untitled-draft", + package: "backend", + title: "Untitled Draft", + status: "draft" as AdrDtoStatus, + supersededBy: null, + tags: [], + deciders: [], + body: { enhancedMdx: "" }, + creationDate: new Date(2020, 4, 4).toJSON(), + lastEditDate: new Date(2020, 10, 26).toJSON(), + lastEditAuthor: "John Doe", + publicationDate: null, + repository: { + provider: "github", + viewUrl: + "https://github.com/foo/bar/blob/master/docs/adr/20200404-untitled-draft.md" + } + }, + { + slug: "backend/20200405-lot-of-deciders", + package: "backend", + title: "Lot of deciders", + status: "draft" as AdrDtoStatus, + supersededBy: null, + tags: [], + deciders: [ + "John Doe", + "Lorem Ipsum", + "Ipsum Dolor", + "Foo Bar", + "John Doe", + "Lorem Ipsum", + "Ipsum Dolor", + "Foo Bar", + "John Doe", + "Lorem Ipsum", + "Ipsum Dolor", + "Foo Bar" + ], + body: { enhancedMdx: "" }, + creationDate: new Date(2020, 4, 5).toJSON(), + lastEditDate: new Date(2020, 10, 26).toJSON(), + lastEditAuthor: "John Doe", + publicationDate: null, + repository: { + provider: "generic", + viewUrl: + "https://custom.com/foo/bar/blob/master/docs/adr/20200405-lot-of-deciders.md" + } + }, + { + slug: "backend/20200405-untitled-draft2", + package: "backend", + title: + "This is a very long title for an ADR which should span on multiple lines but it does not matter", + status: "draft" as AdrDtoStatus, + supersededBy: null, + tags: [], + deciders: ["John Doe", "Lorem Ipsum", "Ipsum Dolor"], + body: { + enhancedMdx: `Hello World +` + }, + creationDate: new Date(2020, 4, 5).toJSON(), + lastEditDate: new Date(2020, 10, 26).toJSON(), + lastEditAuthor: "John Doe", + publicationDate: null, + repository: { + provider: "github", + viewUrl: + "https://github.com/foo/bar/blob/master/docs/adr/20200405-untitled-draft2.md" + } + } +]; +adrMocks.reverse(); + +export function getMockedAdrBySlug(slug: string): Adr | undefined { + return adrMocks.filter((adr) => adr.slug === slug).pop(); +} diff --git a/packages/web/.storybook/mocks/index.ts b/packages/web/.storybook/mocks/index.ts new file mode 100644 index 00000000..816340cf --- /dev/null +++ b/packages/web/.storybook/mocks/index.ts @@ -0,0 +1 @@ +export * from "./adrs"; diff --git a/packages/web/.storybook/preview-head.html b/packages/web/.storybook/preview-head.html new file mode 100644 index 00000000..05a8a741 --- /dev/null +++ b/packages/web/.storybook/preview-head.html @@ -0,0 +1,9 @@ +<link rel="preconnect" href="https://fonts.gstatic.com" /> +<link + href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" + rel="stylesheet" +/> +<link + rel="stylesheet" + href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@300;400;500;700&display=swap" +/> diff --git a/packages/web/.storybook/preview.js b/packages/web/.storybook/preview.js new file mode 100644 index 00000000..96b1cf7f --- /dev/null +++ b/packages/web/.storybook/preview.js @@ -0,0 +1,21 @@ +import React from "react"; +import { MuiDecorator } from "../src/mui"; +import * as nextImage from "next/image"; +import "highlight.js/styles/github.css"; +import "../src/components/Markdown/hljs.css" + +// Fix to make next/image work in Storybook (thanks https://stackoverflow.com/questions/64622746/how-to-mock-next-js-image-component-in-storybook) +Object.defineProperty(nextImage, "default", { + configurable: true, + value: (props) => { + return <img {...props} />; + } +}); + +export const decorators = [ + (Story) => ( + <MuiDecorator> + <Story /> + </MuiDecorator> + ) +]; diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 00000000..fd0228b6 --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,73 @@ +# @log4brains/web + +This package provides the web UI of the [Log4brains](https://github.com/thomvaill/log4brains) architecture knowledge base and its static site generation capabilities. + +## Installation + +You should use `npx init-log4brains` as described in the [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md), which will install all the required dependencies in your project, including this one, and set up the right scripts in your `package.json`. + +You can also install this package manually via npm or yarn: + +```bash +npm install --save-dev @log4brains/web +``` + +or + +```bash +yarn add --dev @log4brains/web +``` + +And add these scripts to your `package.json`: + +```json +{ + [...] + "scripts": { + [...] + "log4brains-preview": "log4brains-web preview", + "log4brains-build": "log4brains-web build", + } +} +``` + +## Usage + +You should have a look at the main [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md) to get more context. + +### Preview + +This command will start the web UI in preview mode locally on <http://localhost:4004/>. +You can define another port with the `-p` option. +In this mode, the Hot Reload feature is enabled: any changes you make to the markdown files are applied live in the UI. + +```bash +npm run log4brains-preview +``` + +or + +```bash +yarn log4brains-preview +``` + +### Build + +This command should be used in your CI/CD pipeline. It creates a static version of your knowledge base, ready to be deployed +on a static website hosting service like GitHub or GitLab pages. + +```bash +npm run log4brains-build +``` + +or + +```bash +yarn log4brains-build +``` + +The default output directory is `.log4brains/out`. You can change it with the `-o` option. + +## Documentation + +- [Log4brains README](https://github.com/thomvaill/log4brains/blob/master/README.md) diff --git a/packages/web/docs/adr/20200925-use-nextjs-for-static-site-generation.md b/packages/web/docs/adr/20200925-use-nextjs-for-static-site-generation.md new file mode 100644 index 00000000..771c2707 --- /dev/null +++ b/packages/web/docs/adr/20200925-use-nextjs-for-static-site-generation.md @@ -0,0 +1,138 @@ +# Use Next.js for Static Site Generation + +- Status: accepted +- Date: 2020-09-25 +- Tags: frontend, frameworks + +## Context and Problem Statement + +Log4brains has two main features: + +- `edit mode` which lets the developer edit the ADRs from a web UI, which is served locally by running `npm run log4brains` + - The developer is also able to edit the markdown files directly from the IDE, which triggers a live-reload of the web UI +- `build mode` which generates a static site ready to deploy on a Github-pages-like service, so that the ADRs are easily browsable. Usually run by the CI with `npm run log4brains-build` + +We need to find the best way to develop these two modes. + +## Decision Drivers <!-- optional --> + +- Maximize code reusability between the two modes (ie. do not have to develop everything twice) +- Time to Market: + - @Thomvaill (first contributor) is a Node/React/PHP developer + - @Thomvaill has 3 weeks available to develop and ship the first version of log4brains +- Balance between completeness/readyness of the chosen solution and future customization + - This first version will have limited features for now but the chosen solution must be customizable enough to be able to implement the future features (ie. we can't choose a 100% opinionated and closed to modifications solution) + +## Considered Options + +- Option 1: MkDocs +- Option 2: Docsify +- Option 3: Docusaurus 2 +- Option 4: Gatsby +- Option 5: Next.js + +Other SSG like Nuxt or Hugo were not considered, because similar to Gatsby and Next.js in terms of features, but developed with other technologies than React. + +## Decision Outcome + +Chosen option: "Option 5: Next.js", because + +- Markdown powered solutions are not enough customizable for our needs +- Gatsby and Next.js are quite similar, so it was hard to choose, but + - Gatsby is more opinionated, because of GraphQL + - I was influenced by this article: [Which To Choose in 2020: NextJS or Gatsby?](https://medium.com/frontend-digest/which-to-choose-in-2020-nextjs-vs-gatsby-1aa7ca279d8a) + +### Positive Consequences <!-- optional --> + +- We will use Typescript because Next.js supports it well + +## Pros and Cons of the Options <!-- optional --> + +### Option 1: MkDocs + +<https://www.mkdocs.org/> + +#### Pros + +- Already powered by Markdown +- Very popular and actively maintained (10.8k stars on Github on 2020-09-22) +- Extendable with plugins and themes +- Live-reload already implemented + +#### Cons + +- `edit mode` can't be developed with a MkDoc plugin, so it has to be developed separately +- Some manual config in `mkdocs.yml` is required for the navigation and/or some [Front Matter](https://jekyllrb.com/docs/front-matter/) config in each markdown file +- Not 100% customizable, even with plugins +- Python + +### Option 2: Docsify + +<https://docsify.js.org/> + +#### Pros + +- Already powered by Markdown +- Very popular and actively maintained (15.1k stars on Github on 2020-09-22) +- Extendable with plugins and themes +- Live-reload already implemented +- No need to generate static pages (lib served over a CDN, which reads directly the markdown files from the repo) -> CI setup simplified + +#### Cons + +- `edit mode` has to be developed separately +- Some manual config in `_nav.yml` is required for the navigation +- Not 100% customizable, even with plugins +- No static pages generation (lib served over a CDN, which reads directly the markdown files from the repo) -> impossible to setup on private projects + +### Option 3: Docusaurus 2 + +<https://v2.docusaurus.io/> + +#### Pros + +- Already powered by Markdown +- Possible to create React pages as well -> good for extensibility +- Very popular and actively maintained (19.1k stars on Github on 2020-09-22), even if the V2 is still in beta +- Live-reload already implemented + +#### Cons + +- No obvious way to develop the `edit mode` without some hacks +- Every markdown file require a [Front Matter](https://jekyllrb.com/docs/front-matter/) header + +### Option 4: Gatsby + +<https://www.gatsbyjs.com/> + +#### Pros + +- Easily extensible SSG framework +- Very popular and actively maintained (47k stars on Github on 2020-09-22) +- `edit mode` can be developed on top of the `gatsby develop` command +- Typescript support + +#### Cons + +- Have to use GraphQL (opinionated framework) +- Need more development to parse markdown files than an already Markdown powered solution + +### Option 5: Next.js + +<https://nextjs.org/> + +#### Pros + +- Easily extensible, non-opinionated SSG framework +- Very popular and actively maintained (53.5k stars on Github on 2020-09-22) +- `edit mode` can be developed on top of the `npm run dev` command +- Typescript support + +#### Cons + +- Need more development to parse markdown files than an already Markdown powered solution + +## Links <!-- optional --> + +- [Curated list of Static Site Generators](https://www.staticgen.com/) used to compare them +- [Which To Choose in 2020: NextJS or Gatsby?](https://medium.com/frontend-digest/which-to-choose-in-2020-nextjs-vs-gatsby-1aa7ca279d8a) diff --git a/packages/web/docs/adr/20200926-react-file-structure-organized-by-feature.md b/packages/web/docs/adr/20200926-react-file-structure-organized-by-feature.md new file mode 100644 index 00000000..33eb6711 --- /dev/null +++ b/packages/web/docs/adr/20200926-react-file-structure-organized-by-feature.md @@ -0,0 +1,8 @@ +# React file structure organized by feature + +- Status: accepted +- Date: 2020-09-26 + +## Decision + +We will follow the structure described in this article: [How to better organize your React applications?](https://medium.com/@alexmngn/how-to-better-organize-your-react-applications-2fd3ea1920f1) by Alexis Mangin. diff --git a/packages/web/docs/adr/20200927-avoid-react-fc-type.md b/packages/web/docs/adr/20200927-avoid-react-fc-type.md new file mode 100644 index 00000000..5d12a395 --- /dev/null +++ b/packages/web/docs/adr/20200927-avoid-react-fc-type.md @@ -0,0 +1,46 @@ +# Avoid React.FC type + +- Status: accepted +- Date: 2020-09-27 +- Source: <https://github.com/spotify/backstage/blob/master/docs/architecture-decisions/adr006-avoid-react-fc.md> <!-- TODO: maybe a new feature? --> + +## Context + +Facebook has removed `React.FC` from their base template for a Typescript +project. The reason for this was that it was found to be an unnecessary feature +with next to no benefits in combination with a few downsides. + +The main reasons were: + +- **children props** were implicitly added +- **Generic Type** was not supported on children + +Read more about the removal in +[this PR](https://github.com/facebook/create-react-app/pull/8177). + +## Decision + +To keep our codebase up to date, we have decided that `React.FC` and `React.SFC` +should be avoided in our codebase when adding new code. + +Here is an example: + +```typescript +/* Avoid this: */ +type BadProps = { text: string }; +const BadComponent: FC<BadProps> = ({ text, children }) => ( + <div> + <div>{text}</div> + {children} + </div> +); + +/* Do this instead: */ +type GoodProps = { text: string; children?: React.ReactNode }; +const GoodComponent = ({ text, children }: GoodProps) => ( + <div> + <div>{text}</div> + {children} + </div> +); +``` diff --git a/packages/web/docs/adr/20200927-use-react-hooks.md b/packages/web/docs/adr/20200927-use-react-hooks.md new file mode 100644 index 00000000..a21fcc9b --- /dev/null +++ b/packages/web/docs/adr/20200927-use-react-hooks.md @@ -0,0 +1,12 @@ +# Use React hooks + +- Status: accepted +- Date: 2020-09-27 + +## Decision + +We will use React hooks and avoid class components. + +## Links + +- [Why We Switched to React Hooks](https://blog.bitsrc.io/why-we-switched-to-react-hooks-48798c42c7f) diff --git a/packages/web/docs/adr/20201007-next-js-persistent-layout-pattern.md b/packages/web/docs/adr/20201007-next-js-persistent-layout-pattern.md new file mode 100644 index 00000000..b8aaed4e --- /dev/null +++ b/packages/web/docs/adr/20201007-next-js-persistent-layout-pattern.md @@ -0,0 +1,12 @@ +# Next.js persistent layout pattern + +- Status: accepted +- Date: 2020-10-07 + +## Context and Problem Statement + +We don't want the menu scroll position to be changed when we navigate from one page to another. + +## Decision + +We will use the [Next.js persistent layout pattern](https://adamwathan.me/2019/10/17/persistent-layout-patterns-in-nextjs/). diff --git a/packages/web/jest.config.js b/packages/web/jest.config.js new file mode 100644 index 00000000..31c317c8 --- /dev/null +++ b/packages/web/jest.config.js @@ -0,0 +1,11 @@ +const base = require("../../jest.config.base"); +const packageJson = require("./package"); + +module.exports = { + ...base, + name: packageJson.name, + displayName: packageJson.name, + transform: { + "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" + } +}; diff --git a/packages/web/next-env.d.ts b/packages/web/next-env.d.ts new file mode 100644 index 00000000..7b7aa2c7 --- /dev/null +++ b/packages/web/next-env.d.ts @@ -0,0 +1,2 @@ +/// <reference types="next" /> +/// <reference types="next/types/global" /> diff --git a/packages/web/next.config.js b/packages/web/next.config.js new file mode 100644 index 00000000..4b21667a --- /dev/null +++ b/packages/web/next.config.js @@ -0,0 +1,44 @@ +const path = require("path"); +const fs = require("fs"); + +const withBundleAnalyzer = require("@next/bundle-analyzer")({ + enabled: process.env.ANALYZE === "true" +}); + +const packageJson = require(`${ + fs.existsSync(path.join(__dirname, "package.json")) ? "./" : "../" +}package.json`); + +module.exports = withBundleAnalyzer({ + reactStrictMode: true, + target: "serverless", + poweredByHeader: false, + serverRuntimeConfig: { + PROJECT_ROOT: __dirname, // https://github.com/vercel/next.js/issues/8251 + VERSION: process.env.HIDE_LOG4BRAINS_VERSION ? "" : packageJson.version + }, + webpack: function (config, { webpack, buildId }) { + // For cache invalidation purpose (thanks https://github.com/vercel/next.js/discussions/14743) + config.plugins.push( + new webpack.DefinePlugin({ + "process.env.NEXT_BUILD_ID": JSON.stringify(buildId) + }) + ); + + // #NEXTJS-HACK + // Fix when the app is running inside `node_modules` (https://github.com/vercel/next.js/issues/19739) + // TODO: remove this fix when this PR is merged: https://github.com/vercel/next.js/pull/19749 + const originalExcludeMethod = config.module.rules[0].exclude; + config.module.rules[0].exclude = (excludePath) => { + if (!originalExcludeMethod(excludePath)) { + return false; + } + return /node_modules/.test(excludePath.replace(config.context, "")); + }; + + return config; + }, + future: { + excludeDefaultMomentLocales: true + } +}); diff --git a/packages/web/nodemon.json b/packages/web/nodemon.json new file mode 100644 index 00000000..b4171b6d --- /dev/null +++ b/packages/web/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["src"], + "ext": "ts,tsx", + "exec": "yarn build:ts" +} diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 00000000..f95d0a97 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,94 @@ +{ + "name": "@log4brains/web", + "version": "1.0.0-beta.0", + "description": "Log4brains architecture knowledge base web UI and static site generator", + "keywords": [ + "log4brains" + ], + "author": "Thomas Vaillant <thomvaill@bluebricks.dev>", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/thomvaill/log4brains", + "repository": { + "type": "git", + "url": "https://github.com/thomvaill/log4brains", + "directory": "packages/web" + }, + "engines": { + "node": ">=10.23.0" + }, + "files": [ + "dist" + ], + "bin": { + "log4brains-web": "./dist/bin/log4brains-web" + }, + "scripts": { + "dev": "yarn build && nodemon", + "dev-old": "cross-env NODE_ENV=development LOG4BRAINS_CWD=../.. ESM_DISABLE_CACHE=true node -r esm -r ts-node/register ./src/bin/main.ts", + "next": "cross-env LOG4BRAINS_CWD=../.. next", + "build:ts": "tsc --noEmit false", + "build": "yarn clean && yarn build:ts && copyfiles --all next.config.js .babelrc 'public/**/*' dist && copyfiles --all --up 1 src/bin/log4brains-web 'src/lib/core-api/noop/**/*' src/components/Markdown/hljs.css dist && cross-env LOG4BRAINS_PHASE=initial-build next build dist", + "clean": "rimraf ./dist", + "serve": "serve .log4brains/out", + "typescript": "tsc", + "test": "jest", + "lint": "eslint . --max-warnings=0", + "storybook": "start-storybook -p 6006", + "prepublishOnly": "yarn build", + "link": "yarn link" + }, + "dependencies": { + "@log4brains/cli-common": "^1.0.0-beta.0", + "@log4brains/core": "^1.0.0-beta.0", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.56", + "@next/bundle-analyzer": "^10.0.1", + "babel-plugin-import": "^1.13.1", + "bufferutil": "^4.0.2", + "chalk": "^4.1.0", + "clsx": "^1.1.1", + "commander": "^6.1.0", + "copy-text-to-clipboard": "^2.2.0", + "esm": "^3.2.25", + "highlight.js": "^10.4.0", + "lunr": "^2.3.9", + "markdown-to-jsx": "^7.0.1", + "mkdirp": "^1.0.4", + "moment": "^2.29.1", + "next": "10.0.1", + "open": "^7.3.0", + "react": "16.13.1", + "react-dom": "16.13.1", + "react-icons": "^3.11.0", + "slugify": "^1.4.5", + "socket.io": "^2.3.0", + "socket.io-client": "^2.3.1", + "utf-8-validate": "^5.0.3" + }, + "devDependencies": { + "@babel/core": "^7.11.6", + "@storybook/addon-actions": "^6.1.9", + "@storybook/addon-essentials": "^6.1.9", + "@storybook/addon-links": "^6.1.9", + "@storybook/react": "^6.1.9", + "@types/lunr": "^2.3.3", + "@types/mkdirp": "^1.0.1", + "@types/react": "^16.9.49", + "@types/react-test-renderer": "^16.9.3", + "@types/signale": "^1.4.1", + "@types/socket.io": "^2.1.11", + "@types/socket.io-client": "^1.4.34", + "@types/url-parse": "^1.4.3", + "babel-jest": "^26.6.0", + "babel-loader": "^8.1.0", + "copyfiles": "^2.4.0", + "react-is": "^16.13.1", + "react-test-renderer": "^17.0.1", + "ts-node": "^9.0.0" + } +} diff --git a/packages/web/public/favicon.ico b/packages/web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..af5b73532d726819a08b819d3179c721643148cb GIT binary patch literal 15086 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 TIMsdI`0$6$^s9_-G}`|I@Esqr literal 0 HcmV?d00001 diff --git a/packages/web/public/l4b-static/Log4brains-logo-dark.png b/packages/web/public/l4b-static/Log4brains-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..13db2655b2f37c3530af81ad027354acda25c08e GIT binary patch literal 22071 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�x7 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 literal 0 HcmV?d00001 diff --git a/packages/web/public/l4b-static/Log4brains-logo.png b/packages/web/public/l4b-static/Log4brains-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7b2a7c1f76125a118013305ac60a31e98df1f294 GIT binary patch literal 62517 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%0Dvfim7(^5Av!o?+;=V%{>~3! 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 JS&4WM@jryvGqL~x literal 0 HcmV?d00001 diff --git a/packages/web/public/l4b-static/Log4brains-og.png b/packages/web/public/l4b-static/Log4brains-og.png new file mode 100644 index 0000000000000000000000000000000000000000..92f5457a944bf564b38807c86e754d0b79014bdc GIT binary patch literal 41376 zcmeFZi93{Q{69V|=#0=}DTO-a#EdObvXwe2v>-FXU{KkHu_WslM5Ch8sRofO5oXMc zZ7|l56j_q7%-DtOBE*F6{pj=gUf+M=cU`}(>(rbw&pgk4zn9nh^?Kd&(Ddp>5x6uQ z27`$hUHaD?2HTzsgKhQSAqf8F-odOD@W)?Xm#lnXFyXz>e_LP)ul9kz+~Q+?@f?iN zd~h86&vs{nD+VxFUX1W>2LTu?RnzES1B-wyQ)|NUw!VSO8~yKhJu^}_x^(E?z4d#h zhu<hHzSxbG6}$C)!?|_h5j;;qP&H9O|HNwH-q$#JR6-#>jdfl*lKSYJOkq=sDONrL zd!+SYRLb|AzqjW&Zr%RiuF;Oe_P@poYT!DFB+JRNW@43HCyS{^oMyEUg*xl`l~osY z{lm6^TLZsNYkvCkBJALicUw1Ko@MN||MRbhI(L8vhJHPD+iv;i)kn>(N`GE`liniv z=hf}UFyTM1*cZaUGlE}XHvj+S{y(yI&qbgOJ9y>t<*wG&r;ndJ;r{wnQcS1Qn@Y>d z2w&0+1z~3oJ_(qCZG-l8*M$og$_MZP0bRm*i1DeZ!oEISxDxE_tv>;>)y#$EJBdE9 zZ(o))mU2CS<=7~>4R#3rCumSB%HfHpJeO*aQq-EZzhE%TpLdDAX*E7UU*+?Y&eP1n zC*veyo43Hcs(1NAw<gQlfqzRuFv5#=2o4I}n4X>%goO!jUNPmNqD|XnBpJPq2si2~ zA|!5s4Mc4Qql>!tXBOOT^z%#b!^l5BY?Nha%0a{FL+-nGo}=yF`hS1c-l&NMKR<}F zw*G%Eptf#<Bx77zJ389Fe!U5U{p<bSM+i#quyfa4GK^_g5>Jl&T{xdTKk?y!rv?m` zc4#w!;7Vf3;Vk#=eDVqg7WUEQ@OJPPxKnU&k8n==Kk@ag4V~THMgan_2LhY<V+w;) z#j%9G^3xdQaE?OB#PqZ#3^sIjGm*Z<W4H79e1f~XQYQyJl2y4C_Wt31C>75(e<Lwa zdT@pY9}WvEF}(}r-M6rCL{p)80P3D(H8v<HXeTU8>rXQ8H~XJkme4Pp_q}t+5`&4` z0^4;Ay91<p*b9jY!(2(0s5-dTu2TpoaKpqOJ$Vw8O5tulpj<eAyhH}}RD5$44;_Y6 zEgV^a3!{;)yK^p7qdYyCTVPMO|M|xYaJmJ7)8F4emx@vp@yJ|-gZDXY)|2Bd-`MCy zg_5=@Bn<ZKoB&ij1}+&;oUqx3UheLn6F3Q)Zs3lO{<-7B*<5Njc029Mz>;HQ?0y)` zD2==eyk*BbW>EtV5l-jFXsn5?+CG1sr<CRUv>PmTy4mI`w5Q-fL{N(wOuRB$sXm=I ztc#peMXt#e-|ZkO31fYq&Vic}{1Xz7!Vx03`U_pJGq~G%$=V&-{>gU^m1tL&mET@p zCQP<pR1$_W+Vyw9&Ps3A!mzQCyep=uoVJPuI&sK?IdA;o*cn%(WY1KRb4F(7{Rt%O zEIuB(jj*?;^z~=R==0~#vv)9zt+(^U?qM=6WKI>L7K(34;}riaMBAT*NI<_v@3nB@ zthWc3c@4dgBBLu)UoP{wZ#;`1J`ybApY-T)fx*%rZpQYNW7n!F*zGNS1s_ZORQ22c zwlvy!RhARFWE-p5RDpMMI{|~W{VRMJtX9GWTsY0J<??c5UR*Pa>NwrQJ7Mjb`wA=c zk|Y4TbU*@%C6|D}9SnREi+X_`ZpF~BWE2<2L2LBaW+b%c&hN6xHq0IS@im9g|K%=~ z9xUC)aF~|n%ElS`#1#CH5la*nEZzpo|95jwrLT5WqgvT$jzT#$Z(y_wE$vQt=}_TG zOcvQG_LBDcwJazI$j!ue*=Hq)QBe-3WDOY)qHK^N)Cv6Xebs_gYtQ^2GQ1{oWRbtG zFW7sl)XlvoLrW@vIe≥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 Q76gJFia1!e|75{`0M>Ew`2YX_ literal 0 HcmV?d00001 diff --git a/packages/web/public/l4b-static/adr-workflow.png b/packages/web/public/l4b-static/adr-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..582a02051cd541772fc281444a53e13022ae0649 GIT binary patch literal 10768 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 literal 0 HcmV?d00001 diff --git a/packages/web/src/bin/log4brains-web b/packages/web/src/bin/log4brains-web new file mode 100755 index 00000000..8bfb3dd2 --- /dev/null +++ b/packages/web/src/bin/log4brains-web @@ -0,0 +1,3 @@ +#!/usr/bin/env node +require = require("esm")(module); +module.exports = require("./main"); diff --git a/packages/web/src/bin/main.ts b/packages/web/src/bin/main.ts new file mode 100644 index 00000000..16062def --- /dev/null +++ b/packages/web/src/bin/main.ts @@ -0,0 +1,58 @@ +import commander from "commander"; +import { previewCommand, buildCommand } from "../cli"; +import { appConsole } from "../lib/console"; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,global-require,@typescript-eslint/no-var-requires +const pkgVersion = require("../../package.json").version as string; + +type StartEditorCommandOpts = { + port: string; + open: boolean; +}; +type BuildCommandOpts = { + out: string; + basePath: string; +}; + +function createCli(version: string): commander.Command { + const program = new commander.Command(); + program.version(version); + + program + .command("preview [adr]") + .description("Start log4brains locally to preview your changes", { + adr: + "If provided, will automatically open your browser to this specific ADR" + }) + .option("-p, --port <port>", "Port to listen on", "4004") + .option("--no-open", "Do not open the browser automatically", false) + .action( + (adr: string, opts: StartEditorCommandOpts): Promise<void> => { + return previewCommand(parseInt(opts.port, 10), opts.open, adr); + } + ); + + program + .command("build") + .description("Build the deployable static website") + .option("-o, --out <path>", "Output path", ".log4brains/out") + .option("--basePath <path>", "Custom base path", "") + .action( + (opts: BuildCommandOpts): Promise<void> => { + return buildCommand(opts.out, opts.basePath); + } + ); + + return program; +} + +const cli = createCli(pkgVersion); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +cli.parseAsync(process.argv).catch((err) => { + if (appConsole.isSpinning()) { + appConsole.stopSpinner(true); + } + appConsole.fatal(err); + process.exit(1); +}); diff --git a/packages/web/src/cli/build.ts b/packages/web/src/cli/build.ts new file mode 100644 index 00000000..3d54c4b0 --- /dev/null +++ b/packages/web/src/cli/build.ts @@ -0,0 +1,117 @@ +import build from "next/dist/build"; +import exportApp from "next/dist/export"; +import loadConfig from "next/dist/next-server/server/config"; +import { PHASE_EXPORT } from "next/dist/next-server/lib/constants"; +import path from "path"; +import mkdirp from "mkdirp"; +import { promises as fsP } from "fs"; +import { getLog4brainsInstance } from "../lib/core-api"; +import { getNextJsDir } from "../lib/next"; +import { appConsole, execNext } from "../lib/console"; +import { Search } from "../lib/search"; +import { toAdrLight } from "../types"; + +export async function buildCommand( + outPath: string, + basePath: string +): Promise<void> { + process.env.NEXT_TELEMETRY_DISABLED = "1"; + appConsole.println("Building Log4brains..."); + + const nextDir = getNextJsDir(); + // eslint-disable-next-line global-require,import/no-dynamic-require,@typescript-eslint/no-var-requires + const nextConfig = require(path.join(nextDir, "next.config.js")) as Record< + string, + unknown + >; + + // We use a different distDir than the preview mode + // because getStaticPath()'s `fallback` config is somehow cached + const distDir = ".next-export"; + const nextCustomConfig = { + ...nextConfig, + distDir, + basePath, + env: { + ...(nextConfig.env && typeof nextConfig.env === "object" + ? nextConfig.env + : {}), + NEXT_PUBLIC_LOG4BRAINS_STATIC: "1" + } + }; + + appConsole.debug("Run `next build`..."); + await execNext(async () => { + // #NEXTJS-HACK: build() is not meant to be called from the outside of Next.js + // And there is an error in their typings: `conf?` is typed as `null`, so we have to use @ts-ignore + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await build(nextDir, nextCustomConfig); + }); + + appConsole.debug("Run `next export`..."); + await execNext(async () => { + await exportApp( + nextDir, + { + outdir: outPath + }, + loadConfig(PHASE_EXPORT, nextDir, nextCustomConfig) // Configuration is not handled like in build() here + ); + }); + + appConsole.startSpinner("Generating ADR JSON data..."); + const buildId = await fsP.readFile( + path.join(nextDir, distDir, "BUILD_ID"), + "utf-8" + ); + + // TODO: move to a dedicated module + await mkdirp(path.join(outPath, "data", buildId)); + const adrs = await getLog4brainsInstance().searchAdrs(); + + // TODO: remove this dead code when we are sure we don't need a JSON file per ADR + + // const packages = new Set<string>(); + // adrs.forEach((adr) => adr.package && packages.add(adr.package)); + // const mkdirpPromises = Array.from(packages).map((pkg) => + // mkdirp(path.join(outPath, `data/adr/${pkg}`)) + // ); + // await Promise.all(mkdirpPromises); + + const promises = [ + // ...adrs.map((adr) => + // fsP.writeFile( + // path.join(outPath, "data", buildId, "adr", `${adr.slug}.json`), + // JSON.stringify( + // toAdr( + // adr, + // adr.supersededBy ? getAdrBySlug(adr.supersededBy, adrs) : undefined + // ) + // ), + // "utf-8" + // ) + // ), + fsP.writeFile( + path.join(outPath, "data", buildId, "adrs.json"), + JSON.stringify(adrs.map(toAdrLight)), + "utf-8" + ) + ]; + await Promise.all(promises); + + appConsole.updateSpinner("Generating search index..."); + await fsP.writeFile( + path.join(outPath, "data", buildId, "search-index.json"), + JSON.stringify(Search.createFromAdrs(adrs).serializeIndex()), + "utf-8" + ); + + appConsole.stopSpinner(); + appConsole.success( + `Your Log4brains static website was successfully built in ${outPath}` + ); + appConsole.println(); + process.exit(0); // otherwise Next.js's spinner keeps running +} diff --git a/packages/web/src/cli/index.ts b/packages/web/src/cli/index.ts new file mode 100644 index 00000000..bcfa55a7 --- /dev/null +++ b/packages/web/src/cli/index.ts @@ -0,0 +1,2 @@ +export * from "./preview"; +export * from "./build"; diff --git a/packages/web/src/cli/preview.ts b/packages/web/src/cli/preview.ts new file mode 100644 index 00000000..9f68c15f --- /dev/null +++ b/packages/web/src/cli/preview.ts @@ -0,0 +1,118 @@ +import next from "next"; +import { createServer } from "http"; +import SocketIO from "socket.io"; +import chalk from "chalk"; +import open from "open"; +import { getLog4brainsInstance } from "../lib/core-api"; +import { getNextJsDir } from "../lib/next"; +import { appConsole, execNext } from "../lib/console"; + +export async function previewCommand( + port: number, + openBrowser: boolean, + adrSlug?: string +): Promise<void> { + process.env.NEXT_TELEMETRY_DISABLED = "1"; + const dev = process.env.NODE_ENV === "development"; + + appConsole.startSpinner("Log4brains is starting..."); + appConsole.debug(`Run \`next ${dev ? "dev" : "start"}\`...`); + + const app = next({ + dev, + dir: getNextJsDir() + }); + + /** + * #NEXTJS-HACK + * We override this private property to set the incrementalCache in "dev" mode (ie. it disables it) + * to make our Hot Reload feature work. + * In fact, we trigger a page re-render every time an ADR changes and we absolutely need up-to-date data on every render. + * The "serve stale data while revalidating" Next.JS policy is not suitable for us. + */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + app.incrementalCache.incrementalOptions.dev = true; // eslint-disable-line @typescript-eslint/no-unsafe-member-access + + await execNext(async () => { + await app.prepare(); + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const srv = createServer(app.getRequestHandler()); + + // FileWatcher with Socket.io + const io = SocketIO(srv); + + const { fileWatcher } = getLog4brainsInstance(); + getLog4brainsInstance().fileWatcher.subscribe((event) => { + appConsole.debug(`[FileWatcher] ${event.type} - ${event.relativePath}`); + io.emit("FileWatcher", event); + }); + fileWatcher.start(); + + try { + await execNext( + () => + new Promise((resolve, reject) => { + // This code catches EADDRINUSE error if the port is already in use + srv.on("error", reject); + srv.on("listening", () => resolve()); + srv.listen(port); + }) + ); + } catch (err) { + appConsole.stopSpinner(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (err.code === "EADDRINUSE") { + if (openBrowser && adrSlug) { + appConsole.println( + chalk.dim( + "Log4brains is already started. We open the browser and exit" + ) + ); + await open(`http://localhost:${port}/adr/${adrSlug}`); + process.exit(0); + } + + appConsole.fatal( + `Port ${port} is already in use. Use the -p <PORT> option to select another one.` + ); + process.exit(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } else if (err.code === "EACCES") { + appConsole.fatal( + `Impossible to use port ${port} (permission denied). Use the -p <PORT> option to select another one.` + ); + process.exit(1); + } else { + throw err; + } + } + + appConsole.stopSpinner(); + appConsole.println( + `Your Log4brains preview is 🚀 on ${chalk.underline.blueBright( + `http://localhost:${port}/` + )}` + ); + appConsole.println( + chalk.dim( + "Hot Reload is enabled: any change you make to a markdown file is applied live" + ) + ); + + if (dev) { + appConsole.println(); + appConsole.println( + `${chalk.bgBlue.white.bold(" DEV ")} ${chalk.blue( + "Next.js' Fast Refresh is enabled" + )}` + ); + appConsole.println(); + } + + if (openBrowser) { + await open(`http://localhost:${port}/${adrSlug ? `adr/${adrSlug}` : ""}`); + } +} diff --git a/packages/web/src/components/AdrStatusChip/AdrStatusChip.stories.tsx b/packages/web/src/components/AdrStatusChip/AdrStatusChip.stories.tsx new file mode 100644 index 00000000..f9b3c204 --- /dev/null +++ b/packages/web/src/components/AdrStatusChip/AdrStatusChip.stories.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { Meta, Story } from "@storybook/react"; +import { AdrStatusChip, AdrStatusChipProps } from "./AdrStatusChip"; + +const Template: Story<AdrStatusChipProps> = (args) => ( + <AdrStatusChip {...args} /> +); + +export default { + title: "AdrStatusChip", + component: AdrStatusChip +} as Meta; + +export const Draft = Template.bind({}); +Draft.args = { status: "draft" }; + +export const Proposed = Template.bind({}); +Proposed.args = { status: "proposed" }; + +export const Rejected = Template.bind({}); +Rejected.args = { status: "rejected" }; + +export const Accepted = Template.bind({}); +Accepted.args = { status: "accepted" }; + +export const Deprecated = Template.bind({}); +Deprecated.args = { status: "deprecated" }; + +export const Superseded = Template.bind({}); +Superseded.args = { status: "superseded" }; diff --git a/packages/web/src/components/AdrStatusChip/AdrStatusChip.tsx b/packages/web/src/components/AdrStatusChip/AdrStatusChip.tsx new file mode 100644 index 00000000..46d2721d --- /dev/null +++ b/packages/web/src/components/AdrStatusChip/AdrStatusChip.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Chip } from "@material-ui/core"; +import { + grey, + indigo, + deepOrange, + lightGreen, + brown +} from "@material-ui/core/colors"; +import { createStyles, Theme, makeStyles } from "@material-ui/core/styles"; +import type { AdrDtoStatus } from "@log4brains/core"; +import clsx from "clsx"; + +// Styles are inspired by the MUI "Badge" styles +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + fontSize: "0.74rem", + fontWeight: theme.typography.fontWeightMedium, + height: "18px", + verticalAlign: "text-bottom" + }, + label: { + padding: "0 6px" + }, + draft: { + color: grey[800] + }, + proposed: { + color: indigo[800] + }, + rejected: { + color: deepOrange[800] + }, + accepted: { + color: lightGreen[800] + }, + deprecated: { + color: brown[600] + }, + superseded: { + color: brown[600] + } + }) +); + +export type AdrStatusChipProps = { + className?: string; + status: AdrDtoStatus; +}; + +export function AdrStatusChip({ className, status }: AdrStatusChipProps) { + const classes = useStyles(); + return ( + <Chip + variant="outlined" + size="small" + label={status.toUpperCase()} + className={clsx(className, classes.root, classes[status])} + classes={{ labelSmall: classes.label }} + /> + ); +} diff --git a/packages/web/src/components/AdrStatusChip/index.ts b/packages/web/src/components/AdrStatusChip/index.ts new file mode 100644 index 00000000..5f6eee42 --- /dev/null +++ b/packages/web/src/components/AdrStatusChip/index.ts @@ -0,0 +1 @@ +export * from "./AdrStatusChip"; diff --git a/packages/web/src/components/Markdown/Markdown.stories.tsx b/packages/web/src/components/Markdown/Markdown.stories.tsx new file mode 100644 index 00000000..45e817d6 --- /dev/null +++ b/packages/web/src/components/Markdown/Markdown.stories.tsx @@ -0,0 +1,141 @@ +import React from "react"; +import { Meta } from "@storybook/react"; +import { Markdown } from "./Markdown"; + +export default { + title: "Markdown", + component: Markdown, + decorators: [ + (DecoratedStory) => ( + <div style={{ width: 750, margin: "auto" }}> + <DecoratedStory /> + </div> + ) + ] +} as Meta; + +export function Default() { + return ( + <Markdown> + {`# Header 1 + +## Header 2 + +### Header 3 + +#### Header 4 + +# Two Paragraphs + +Lorem ipsum dolor [sit amet](#), consectetur adipiscing elit. \`Aenean convallis lorem eu volutpat congue\`. Cras rutrum porta nisi, vel hendrerit ante. Quisque imperdiet semper lectus ut luctus. Mauris eu sollicitudin erat, sit amet consequat tortor. Nam quis placerat nisi. Proin ornare dui vel quam luctus, rutrum mollis lectus tincidunt. Maecenas commodo maximus nisi, quis gravida urna lobortis ac. In ac eleifend felis, vel tincidunt elit. Donec consectetur sapien quis lacus congue congue. Fusce sagittis aliquam ex. Etiam euismod, orci sit amet vulputate rhoncus, lacus tellus blandit sem, id tristique lectus massa commodo nibh. Nunc varius lorem mattis enim laoreet imperdiet laoreet at mi. + +Donec felis ex, auctor a ligula quis, mollis sollicitudin libero. Aliquam vitae egestas quam. Vivamus porta suscipit eros, a hendrerit mi finibus vel. Sed vestibulum ante vel lacinia pellentesque. Integer elementum ultricies ante, vel gravida arcu faucibus et. Nunc tristique egestas pellentesque. Morbi volutpat dictum mi, quis ultricies leo. Morbi rhoncus malesuada lectus ut egestas. Phasellus finibus sodales nunc vel blandit. In tellus augue, posuere non libero eget, rhoncus tempus dui. Proin consequat libero ac felis volutpat, nec tempor ipsum dapibus. In vitae arcu efficitur, venenatis ipsum sit amet, tempus massa. + +# Code + +## Raw + +\`\`\` +Donec felis ex, auctor a ligula quis, mollis sollicitudin libero. +Aliquam vitae egestas quam. Vivamus porta suscipit eros, a hendrerit mi finibus vel. + +Sed vestibulum ante vel lacinia pellentesque. Integer elementum ultricies ante, vel gravida arcu faucibus et. +Nunc tristique egestas pellentesque. Morbi volutpat dictum mi, quis ultricies leo. +Morbi rhoncus malesuada lectus ut egestas. Phasellus finibus sodales nunc vel blandit. +In tellus augue, posuere non libero eget, rhoncus tempus dui. +Proin consequat libero ac felis volutpat, nec tempor ipsum dapibus. In vitae arcu efficitur, venenatis ipsum sit amet, tempus massa. +\`\`\` + +## JS + +\`\`\`javascript +const express = require("express"); +const app = express(); +const port = 3000; + +app.get("/", (req, res) => { + res.send("Hello World!"); +}); + +app.listen(port, () => { + console.log(\`Example app listening at http://localhost:\${port}\`); +}); +\`\`\` + +## TS + +\`\`\`typescript +interface User { + name: string; + id: number; +} + +class UserAccount { + name: string; + id: number; + + constructor(name: string, id: number) { + this.name = name; + this.id = id; + } +} + +const user: User = new UserAccount("Murphy", 1); +\`\`\` + +## JSON + +\`\`\`json +{ + "compilerOptions": { + "target": "ES2018", + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "preserve" + } +} +\`\`\` + +## PHP + +\`\`\`php +<html> + <head> + <title>Test PHP + + + Bonjour le monde

'; ?> + + +\`\`\` + +# Lists + +## Unordered + +- Item +- Item +- Item + +## Ordered + +1. Item +2. Item +3. Item + +## Long items + +- Lorem ipsum dolor [sit amet](#), consectetur adipiscing elit. Aenean convallis lorem eu volutpat congue. Cras rutrum porta nisi, vel hendrerit ante. Quisque imperdiet semper lectus ut luctus. Mauris eu sollicitudin erat, sit amet consequat tortor. Nam quis placerat nisi. Proin ornare dui vel quam luctus, rutrum mollis lectus tincidunt. +- Maecenas commodo maximus nisi, quis gravida urna lobortis ac. In ac eleifend felis, vel tincidunt elit. Donec consectetur sapien quis lacus congue congue. Fusce sagittis aliquam ex. Etiam euismod, orci sit amet vulputate rhoncus, lacus tellus blandit sem, id tristique lectus massa commodo nibh. Nunc varius lorem mattis enim laoreet imperdiet laoreet at mi. +- Donec felis ex, auctor a ligula quis, mollis sollicitudin libero. Aliquam vitae egestas quam. Vivamus porta suscipit eros, a hendrerit mi finibus vel. Sed vestibulum ante vel lacinia pellentesque. Integer elementum ultricies ante, vel gravida arcu faucibus et. Nunc tristique egestas pellentesque. Morbi volutpat dictum mi, quis ultricies leo. +- Morbi rhoncus malesuada lectus ut egestas. Phasellus finibus sodales nunc vel blandit. In tellus augue, posuere non libero eget, rhoncus tempus dui. Proin consequat libero ac felis volutpat, nec tempor ipsum dapibus. In vitae arcu efficitur, venenatis ipsum sit amet, tempus massa. + +`} + + ); +} diff --git a/packages/web/src/components/Markdown/Markdown.tsx b/packages/web/src/components/Markdown/Markdown.tsx new file mode 100644 index 00000000..66e9befe --- /dev/null +++ b/packages/web/src/components/Markdown/Markdown.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useMemo } from "react"; +import { compiler as mdCompiler } from "markdown-to-jsx"; +import { useRouter } from "next/router"; +import hljs from "highlight.js"; +import { makeStyles, createStyles } from "@material-ui/core/styles"; +import { + Typography, + Link as MuiLink, + TypographyProps +} from "@material-ui/core"; +import { CustomTheme } from "../../mui"; +import { AdrLink } from "./components"; +import { MarkdownHeading } from "../MarkdownHeading"; +import { slugify } from "../../lib/slugify"; + +const useStyles = makeStyles((theme: CustomTheme) => + createStyles({ + code: { + backgroundColor: "#F8F8F8", + borderRadius: theme.shape.borderRadius, + padding: 3 + }, + listItem: {} + }) +); + +function Li(props: TypographyProps) { + const classes = useStyles(); + return ( +
  • + +
  • + ); +} + +function Code(props: { children: React.ReactNode }) { + const classes = useStyles(); + const { children } = props; + return {children}; +} + +const options = { + overrides: { + h1: { + component: Typography, + props: { variant: "h3", component: "h1", gutterBottom: true } + }, + h2: { + component: MarkdownHeading, + props: { variant: "h2" } + }, + h3: { + component: MarkdownHeading, + props: { variant: "h3" } + }, + h4: { + component: MarkdownHeading, + props: { variant: "h4" } + }, + p: { component: Typography, props: { paragraph: true } }, + a: { component: MuiLink }, + li: { component: Li }, + AdrLink: { component: AdrLink }, + code: { component: Code } + }, + slugify +}; + +type MarkdownProps = { + children: string; + onCompiled?: (content: React.ReactElement) => void; +}; + +function isReactElementWithChildren( + obj: JSX.Element +): obj is React.ReactElement<{ children: React.ReactElement }> { + return "children" in obj.props; // TODO: improve tests here +} + +export function Markdown({ children, onCompiled }: MarkdownProps) { + const rootRef = React.useRef(null); + + const router = useRouter(); + + const renderedMarkdown = useMemo( + () => + mdCompiler( + children.replace( + // Fix for `index.md`'s adr-workflow.png image path + // TODO: support local images (https://github.com/thomvaill/log4brains/issues/4) + /\((\/l4b-static\/[^)]+)\)/g, + `(${router?.basePath}$1)` + ), + options + ), + [children, router] + ); + + useEffect(() => { + if (onCompiled && isReactElementWithChildren(renderedMarkdown)) { + onCompiled(renderedMarkdown.props.children); + } + }, [children, renderedMarkdown, onCompiled]); + + useEffect(() => { + if (isReactElementWithChildren(renderedMarkdown)) { + rootRef.current + ?.querySelectorAll("pre code") + .forEach((block) => { + hljs.highlightBlock(block); + }); + } + }, [children, renderedMarkdown]); + + return
    {renderedMarkdown}
    ; +} diff --git a/packages/web/src/components/Markdown/components/AdrLink/AdrLink.tsx b/packages/web/src/components/Markdown/components/AdrLink/AdrLink.tsx new file mode 100644 index 00000000..1dbc7af8 --- /dev/null +++ b/packages/web/src/components/Markdown/components/AdrLink/AdrLink.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { AdrDtoStatus } from "@log4brains/core"; +import { makeStyles, createStyles } from "@material-ui/core/styles"; +import { Link as MuiLink } from "@material-ui/core"; +import Link from "next/link"; +import clsx from "clsx"; + +const useStyles = makeStyles(() => + createStyles({ + // TODO: refactor with AdrMenu.tsx + draftLink: {}, + proposedLink: {}, + acceptedLink: {}, + rejectedLink: { + textDecoration: "line-through" + }, + deprecatedLink: { + textDecoration: "line-through" + }, + supersededLink: { + textDecoration: "line-through" + } + }) +); + +type AdrLinkProps = { + slug: string; + status: AdrDtoStatus; + // eslint-disable-next-line react/no-unused-prop-types + package?: string; + title?: string; + customLabel?: string; +}; + +export function AdrLink({ slug, status, title, customLabel }: AdrLinkProps) { + const classes = useStyles(); + + return ( + + + {customLabel || title || "Untitled"} + + + ); +} diff --git a/packages/web/src/components/Markdown/components/AdrLink/index.ts b/packages/web/src/components/Markdown/components/AdrLink/index.ts new file mode 100644 index 00000000..9c2406e9 --- /dev/null +++ b/packages/web/src/components/Markdown/components/AdrLink/index.ts @@ -0,0 +1 @@ +export * from "./AdrLink"; diff --git a/packages/web/src/components/Markdown/components/index.ts b/packages/web/src/components/Markdown/components/index.ts new file mode 100644 index 00000000..9c2406e9 --- /dev/null +++ b/packages/web/src/components/Markdown/components/index.ts @@ -0,0 +1 @@ +export * from "./AdrLink"; diff --git a/packages/web/src/components/Markdown/hljs.css b/packages/web/src/components/Markdown/hljs.css new file mode 100644 index 00000000..cc313157 --- /dev/null +++ b/packages/web/src/components/Markdown/hljs.css @@ -0,0 +1,3 @@ +.hljs { + padding: 14px !important; +} diff --git a/packages/web/src/components/Markdown/index.ts b/packages/web/src/components/Markdown/index.ts new file mode 100644 index 00000000..3306b090 --- /dev/null +++ b/packages/web/src/components/Markdown/index.ts @@ -0,0 +1 @@ +export * from "./Markdown"; diff --git a/packages/web/src/components/MarkdownHeading/MarkdownHeading.tsx b/packages/web/src/components/MarkdownHeading/MarkdownHeading.tsx new file mode 100644 index 00000000..068b77a8 --- /dev/null +++ b/packages/web/src/components/MarkdownHeading/MarkdownHeading.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; +import { + Typography, + TypographyClassKey, + Link as MuiLink +} from "@material-ui/core"; +import clsx from "clsx"; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + "&:hover": { + "& $link": { + visibility: "visible" + } + } + }, + link: { + marginLeft: "0.3ch", + color: "inherit", + "&:hover": { + color: theme.palette.primary.main + }, + visibility: "hidden" + } + }) +); + +export type MarkdownHeadingProps = { + children: string; + id: string; + variant: "h1" | "h2" | "h3" | "h4"; + className?: string; +}; + +export function MarkdownHeading({ + id, + children, + variant, + className +}: MarkdownHeadingProps) { + const classes = useStyles(); + + let typographyVariant: TypographyClassKey; + switch (variant) { + case "h1": + typographyVariant = "h3"; + break; + case "h2": + typographyVariant = "h4"; + break; + case "h3": + typographyVariant = "h5"; + break; + case "h4": + typographyVariant = "h6"; + break; + default: + typographyVariant = "h6"; + break; + } + + return ( + + {children} + + ¶ + + + ); +} diff --git a/packages/web/src/components/MarkdownHeading/index.ts b/packages/web/src/components/MarkdownHeading/index.ts new file mode 100644 index 00000000..331f6474 --- /dev/null +++ b/packages/web/src/components/MarkdownHeading/index.ts @@ -0,0 +1 @@ +export * from "./MarkdownHeading"; diff --git a/packages/web/src/components/MarkdownToc/MarkdownToc.stories.tsx b/packages/web/src/components/MarkdownToc/MarkdownToc.stories.tsx new file mode 100644 index 00000000..fcbdbaa4 --- /dev/null +++ b/packages/web/src/components/MarkdownToc/MarkdownToc.stories.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { Meta } from "@storybook/react"; +import { compiler as mdCompiler } from "markdown-to-jsx"; +import { MarkdownHeading } from "../MarkdownHeading"; +import { MarkdownToc } from "./MarkdownToc"; + +const markdown = `# Header 1 +Lorem Ipsum + +## Header 1.1 + +### Header 1.1.1 + +### Header 1.1.2 + +## Header 1.2 + +#### Subtitle without direct parent + +test + +# Header 2 + +hello`; + +const options = { + overrides: { + h1: { + component: MarkdownHeading, + props: { variant: "h1" } + }, + h2: { + component: MarkdownHeading, + props: { variant: "h2" } + }, + h3: { + component: MarkdownHeading, + props: { variant: "h3" } + }, + h4: { + component: MarkdownHeading, + props: { variant: "h4" } + } + } +}; + +const content = mdCompiler(markdown, options) as React.ReactElement<{ + children: React.ReactElement; +}>; + +export default { + title: "MarkdownToc", + component: MarkdownToc +} as Meta; + +export function Default() { + return ; +} diff --git a/packages/web/src/components/MarkdownToc/MarkdownToc.test.tsx b/packages/web/src/components/MarkdownToc/MarkdownToc.test.tsx new file mode 100644 index 00000000..6286f309 --- /dev/null +++ b/packages/web/src/components/MarkdownToc/MarkdownToc.test.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { compiler as mdCompiler } from "markdown-to-jsx"; +import TestRenderer from "react-test-renderer"; +import { MarkdownHeading } from "../MarkdownHeading"; +import { MarkdownToc } from "./MarkdownToc"; + +const markdown = `# Header 1 +Lorem Ipsum + +## Header 1.1 + +### Header 1.1.1 + +### Header 1.1.2 + +## Header 1.2 + +#### Subtitle without direct parent + +test + +# Header 2 + +hello`; + +const options = { + overrides: { + h1: { + component: MarkdownHeading, + props: { variant: "h1" } + }, + h2: { + component: MarkdownHeading, + props: { variant: "h2" } + }, + h3: { + component: MarkdownHeading, + props: { variant: "h3" } + }, + h4: { + component: MarkdownHeading, + props: { variant: "h4" } + } + } +}; + +describe("Toc", () => { + const content = mdCompiler(markdown, options) as React.ReactElement<{ + children: React.ReactElement; + }>; + + it("renders correctly", () => { + const tree = TestRenderer.create( + + ); + expect(tree.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/packages/web/src/components/MarkdownToc/MarkdownToc.tsx b/packages/web/src/components/MarkdownToc/MarkdownToc.tsx new file mode 100644 index 00000000..1551908f --- /dev/null +++ b/packages/web/src/components/MarkdownToc/MarkdownToc.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; +import { Link as MuiLink, Typography } from "@material-ui/core"; +import clsx from "clsx"; +import { + Toc as TocModel, + TocBuilder as TocModelBuilder +} from "../../lib/toc-utils"; +import { MarkdownHeading, MarkdownHeadingProps } from "../MarkdownHeading"; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + "& > ul": { + padding: "0 !important" + } + }, + title: { + fontWeight: theme.typography.fontWeightBold, + paddingBottom: theme.spacing(1) + }, + tocUl: { + listStyleType: "none", + paddingLeft: "1rem" + } + }) +); + +function variantToLevel(variant: string): number { + return parseInt(variant.replace("h", ""), 10); +} + +function isMarkdownHeadingElement( + element: JSX.Element +): element is React.ReactElement { + return typeof element.type === "function" && element.type === MarkdownHeading; +} + +function buildTocModelFromContent( + content: React.ReactElement, + levelStart = 1 +): TocModel { + const builder = new TocModelBuilder(); + React.Children.forEach(content, (element) => { + if (isMarkdownHeadingElement(element)) { + builder.addSection( + variantToLevel(element.props.variant) - levelStart + 1, + element.props.children, + element.props.id + ); + } + }); + + return builder.getToc(); +} + +type TocSectionProps = { + children: React.ReactNode; + title: string; + id: string; +}; + +function TocSection({ title, id, children }: TocSectionProps) { + const classes = useStyles(); + + return ( +
  • + {title} + {children ?
      {children}
    : null} +
  • + ); +} + +export type MarkdownTocProps = { + className?: string; + content?: React.ReactElement; + levelStart?: number; +}; + +export function MarkdownToc({ + className, + content, + levelStart = 1 +}: MarkdownTocProps) { + const classes = useStyles(); + + if (!content) { + return null; + } + + const model = buildTocModelFromContent(content, levelStart); + if (model.children.length === 0) { + return null; + } + + return ( +
    + + Table of contents + +
      + {model.render((title: string, id: string, children: JSX.Element[]) => ( + + {children} + + ))} +
    +
    + ); +} diff --git a/packages/web/src/components/MarkdownToc/__snapshots__/MarkdownToc.test.tsx.snap b/packages/web/src/components/MarkdownToc/__snapshots__/MarkdownToc.test.tsx.snap new file mode 100644 index 00000000..befbd168 --- /dev/null +++ b/packages/web/src/components/MarkdownToc/__snapshots__/MarkdownToc.test.tsx.snap @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Toc renders correctly 1`] = ` +
    +`; diff --git a/packages/web/src/components/MarkdownToc/index.ts b/packages/web/src/components/MarkdownToc/index.ts new file mode 100644 index 00000000..3ce98894 --- /dev/null +++ b/packages/web/src/components/MarkdownToc/index.ts @@ -0,0 +1 @@ +export * from "./MarkdownToc"; diff --git a/packages/web/src/components/SearchBox/SearchBox.stories.tsx b/packages/web/src/components/SearchBox/SearchBox.stories.tsx new file mode 100644 index 00000000..6c45a434 --- /dev/null +++ b/packages/web/src/components/SearchBox/SearchBox.stories.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { Meta, Story } from "@storybook/react"; +import { AppBar, Toolbar } from "@material-ui/core"; +import { SearchBox, SearchBoxProps } from "./SearchBox"; +import { adrMocks } from "../../../.storybook/mocks"; + +const Template: Story = (args) => ; + +export default { + title: "SearchBox", + component: SearchBox, + decorators: [ + (DecoratedStory) => ( + + +
    + +
    +
    +
    + ) + ] +} as Meta; + +export const Closed = Template.bind({}); +Closed.args = {}; + +export const Open = Template.bind({}); +Open.args = { + open: true +}; + +export const OpenWithResults = Template.bind({}); +OpenWithResults.args = { + open: true, + query: "Test", + results: adrMocks.map((adr) => ({ + title: adr.title, + href: `/adr/${adr.slug}` + })) +}; + +export const OpenLoading = Template.bind({}); +OpenLoading.args = { + open: true, + query: "test", + results: [], + loading: true +}; + +export const OpenWithoutResults = Template.bind({}); +OpenWithoutResults.args = { + open: true, + query: "cdlifsdilhfsd", + results: [] +}; diff --git a/packages/web/src/components/SearchBox/SearchBox.tsx b/packages/web/src/components/SearchBox/SearchBox.tsx new file mode 100644 index 00000000..65f3f65b --- /dev/null +++ b/packages/web/src/components/SearchBox/SearchBox.tsx @@ -0,0 +1,196 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import React from "react"; +import { + Autocomplete, + AutocompleteCloseReason, + AutocompleteInputChangeReason, + AutocompleteProps +} from "@material-ui/lab"; +import { CircularProgress, SvgIcon, Typography } from "@material-ui/core"; +import { useControlled } from "@material-ui/core/utils"; +import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; +import { GrDocumentText as AdrIcon } from "react-icons/gr"; +import { useRouter } from "next/router"; +import { SearchBar } from "./components/SearchBar"; +import { SearchResult } from "../../lib/search"; + +const useStyles = makeStyles((theme: Theme) => { + return createStyles({ + searchBar: { + zIndex: "inherit" + }, + resultTitle: { + marginLeft: "0.5ch" + }, + acPaper: { + borderRadius: `0 0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px`, + marginTop: 0 + } + }); +}); + +export type SearchBoxProps = Omit< + AutocompleteProps, + "results" | "renderInput" | "options" +> & { + /** + * Callback fired when the search box requests to be opened. + * Used in controlled mode (see open). + * + * @param event The event source of the callback. + */ + onOpen?: (event: React.ChangeEvent<{}>) => void; + + /** + * Callback fired when the popup requests to be closed. + * Used in controlled mode (see open). + * + * @param event The event source of the callback. + * @param reason Can be: `"toggleInput"`, `"escape"`, `"select-option"`, `"blur"`. + */ + onClose?: ( + event: React.ChangeEvent<{}>, + reason: AutocompleteCloseReason + ) => void; + + /** + * Control the popup open state. + * Set -> controlled mode, unset -> uncontrolled mode. + */ + open?: boolean; + + /** + * Callback fired when the search query changes. + * Controlled mode only. + * + * @param event The event source of the callback. + * @param query The new value of the search query. + * @param reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`. + */ + onQueryChange?: ( + event: React.ChangeEvent<{}>, + query: string, + reason: AutocompleteInputChangeReason + ) => void; + + /** + * The search query. + * Controlled mode only. + */ + query?: string; + + /** + * The search results. + * Controlled mode only. + */ + results?: SearchResult[]; + + /** + * To display a spinner. + */ + loading?: boolean; +}; + +export function SearchBox(props: SearchBoxProps) { + const classes = useStyles(); + + const { + onOpen, + onClose, + open: openProp, + onQueryChange, + query, + results, + loading = false, + ...otherProps + } = props; + + const [open, setOpenState] = useControlled({ + controlled: openProp, + default: false, + name: "SearchBox", + state: "open" + }); + + const handleOpen = (event: React.ChangeEvent<{}>) => { + if (open) { + return; + } + setOpenState(true); + if (onOpen) { + onOpen(event); + } + }; + + const router = useRouter(); + + const handleClose = ( + event: React.ChangeEvent<{}>, + reason: AutocompleteCloseReason + ) => { + if (!open) { + return; + } + setOpenState(false); + if (onClose) { + onClose(event, reason); + } + }; + + let noOptionsText: React.ReactNode = "Type to start searching"; + if (loading) { + noOptionsText = ( +
    + +
    + ); + } else if (query) { + noOptionsText = "No matching documents"; + } + + return ( + result.title} + renderInput={(params) => ( + + onQueryChange && onQueryChange(event, "", "clear") + } + className={classes.searchBar} + /> + )} + inputValue={query} + onInputChange={(event, value, reason) => { + // We don't want to replace the inputValue by the selected value + if (reason !== "reset" && onQueryChange) { + onQueryChange(event, value, reason); + } + }} + open={open} + onOpen={handleOpen} + onClose={handleClose} + filterOptions={(r) => r} // We hijack Autocomplete's behavior to display search results as options + renderOption={(result) => ( + <> + + + + + {result.title} + + + )} + noOptionsText={noOptionsText} + onChange={async (_, result) => { + if (result) { + await router.push(result.href); + } + }} + /> + ); +} diff --git a/packages/web/src/components/SearchBox/components/SearchBar/SearchBar.tsx b/packages/web/src/components/SearchBox/components/SearchBar/SearchBar.tsx new file mode 100644 index 00000000..c704e1bd --- /dev/null +++ b/packages/web/src/components/SearchBox/components/SearchBar/SearchBar.tsx @@ -0,0 +1,89 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import React from "react"; +import { + InputBase, + InputAdornment, + InputBaseProps, + IconButton, + Fade +} from "@material-ui/core"; +import { + createStyles, + makeStyles, + Theme, + fade +} from "@material-ui/core/styles"; +import { Search as SearchIcon, Close as ClearIcon } from "@material-ui/icons"; +import { AutocompleteRenderInputParams } from "@material-ui/lab"; + +export type SearchBarProps = InputBaseProps & + AutocompleteRenderInputParams & { + open: boolean; + onClear: (event: React.ChangeEvent<{}>) => void; + }; + +// Inspired by https://material-ui.com/components/app-bar/#app-bar-with-search-field +const useStyles = makeStyles((theme: Theme) => { + return createStyles({ + inputRoot: ({ open }: SearchBarProps) => ({ + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + borderRadius: open + ? `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0 0` + : `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px`, + color: open + ? theme.palette.getContrastText(theme.palette.common.white) + : "inherit", + backgroundColor: open + ? theme.palette.common.white + : fade(theme.palette.common.white, 0.15), + "&:hover": { + backgroundColor: open + ? theme.palette.common.white + : fade(theme.palette.common.white, 0.25) + } + }), + inputInput: { + padding: theme.spacing(1, 1, 1, 0) + }, + clearIcon: { + color: "inherit" + } + }); +}); + +export function SearchBar(props: SearchBarProps) { + const { InputProps, InputLabelProps, open, onClear, ...params } = props; + const classes = useStyles(props); + return ( + + + + } + endAdornment={ + // eslint-disable-next-line react/destructuring-assignment + + + onClear(event)} + size="small" + title="Clear" + className={classes.clearIcon} + > + + + + + } + ref={InputProps.ref} + {...params} + /> + ); +} diff --git a/packages/web/src/components/SearchBox/components/SearchBar/index.ts b/packages/web/src/components/SearchBox/components/SearchBar/index.ts new file mode 100644 index 00000000..f9dfce51 --- /dev/null +++ b/packages/web/src/components/SearchBox/components/SearchBar/index.ts @@ -0,0 +1 @@ +export * from "./SearchBar"; diff --git a/packages/web/src/components/SearchBox/index.ts b/packages/web/src/components/SearchBox/index.ts new file mode 100644 index 00000000..3e0944e0 --- /dev/null +++ b/packages/web/src/components/SearchBox/index.ts @@ -0,0 +1 @@ +export * from "./SearchBox"; diff --git a/packages/web/src/components/TwoColContent/TwoColContent.stories.tsx b/packages/web/src/components/TwoColContent/TwoColContent.stories.tsx new file mode 100644 index 00000000..54dbb3b4 --- /dev/null +++ b/packages/web/src/components/TwoColContent/TwoColContent.stories.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import { Meta } from "@storybook/react"; +import { Typography } from "@material-ui/core"; +import { TwoColContent } from "./TwoColContent"; + +export default { + title: "TwoColContent", + component: TwoColContent +} as Meta; + +export function OneColumn() { + return ( + + + Lorem Ipsum + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean + convallis lorem eu volutpat congue. Cras rutrum porta nisi, vel + hendrerit ante. Quisque imperdiet semper lectus ut luctus. Mauris eu + sollicitudin erat, sit amet consequat tortor. Nam quis placerat nisi. + Proin ornare dui vel quam luctus, rutrum mollis lectus tincidunt. + Maecenas commodo maximus nisi, quis gravida urna lobortis ac. In ac + eleifend felis, vel tincidunt elit. Donec consectetur sapien quis lacus + congue congue. Fusce sagittis aliquam ex. Etiam euismod, orci sit amet + vulputate rhoncus, lacus tellus blandit sem, id tristique lectus massa + commodo nibh. Nunc varius lorem mattis enim laoreet imperdiet laoreet at + mi. + + + Donec felis ex, auctor a ligula quis, mollis sollicitudin libero. + Aliquam vitae egestas quam. Vivamus porta suscipit eros, a hendrerit mi + finibus vel. Sed vestibulum ante vel lacinia pellentesque. Integer + elementum ultricies ante, vel gravida arcu faucibus et. Nunc tristique + egestas pellentesque. Morbi volutpat dictum mi, quis ultricies leo. + Morbi rhoncus malesuada lectus ut egestas. Phasellus finibus sodales + nunc vel blandit. In tellus augue, posuere non libero eget, rhoncus + tempus dui. Proin consequat libero ac felis volutpat, nec tempor ipsum + dapibus. In vitae arcu efficitur, venenatis ipsum sit amet, tempus + massa. + + + ); +} + +export function TwoColumns() { + return ( + Some content}> + + Lorem Ipsum + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean + convallis lorem eu volutpat congue. Cras rutrum porta nisi, vel + hendrerit ante. Quisque imperdiet semper lectus ut luctus. Mauris eu + sollicitudin erat, sit amet consequat tortor. Nam quis placerat nisi. + Proin ornare dui vel quam luctus, rutrum mollis lectus tincidunt. + Maecenas commodo maximus nisi, quis gravida urna lobortis ac. In ac + eleifend felis, vel tincidunt elit. Donec consectetur sapien quis lacus + congue congue. Fusce sagittis aliquam ex. Etiam euismod, orci sit amet + vulputate rhoncus, lacus tellus blandit sem, id tristique lectus massa + commodo nibh. Nunc varius lorem mattis enim laoreet imperdiet laoreet at + mi. + + + Donec felis ex, auctor a ligula quis, mollis sollicitudin libero. + Aliquam vitae egestas quam. Vivamus porta suscipit eros, a hendrerit mi + finibus vel. Sed vestibulum ante vel lacinia pellentesque. Integer + elementum ultricies ante, vel gravida arcu faucibus et. Nunc tristique + egestas pellentesque. Morbi volutpat dictum mi, quis ultricies leo. + Morbi rhoncus malesuada lectus ut egestas. Phasellus finibus sodales + nunc vel blandit. In tellus augue, posuere non libero eget, rhoncus + tempus dui. Proin consequat libero ac felis volutpat, nec tempor ipsum + dapibus. In vitae arcu efficitur, venenatis ipsum sit amet, tempus + massa. + + + ); +} + +export function TwoColumnsWithTitle() { + return ( + Some content} + rightColTitle="Column title" + > + + Lorem Ipsum + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean + convallis lorem eu volutpat congue. Cras rutrum porta nisi, vel + hendrerit ante. Quisque imperdiet semper lectus ut luctus. Mauris eu + sollicitudin erat, sit amet consequat tortor. Nam quis placerat nisi. + Proin ornare dui vel quam luctus, rutrum mollis lectus tincidunt. + Maecenas commodo maximus nisi, quis gravida urna lobortis ac. In ac + eleifend felis, vel tincidunt elit. Donec consectetur sapien quis lacus + congue congue. Fusce sagittis aliquam ex. Etiam euismod, orci sit amet + vulputate rhoncus, lacus tellus blandit sem, id tristique lectus massa + commodo nibh. Nunc varius lorem mattis enim laoreet imperdiet laoreet at + mi. + + + Donec felis ex, auctor a ligula quis, mollis sollicitudin libero. + Aliquam vitae egestas quam. Vivamus porta suscipit eros, a hendrerit mi + finibus vel. Sed vestibulum ante vel lacinia pellentesque. Integer + elementum ultricies ante, vel gravida arcu faucibus et. Nunc tristique + egestas pellentesque. Morbi volutpat dictum mi, quis ultricies leo. + Morbi rhoncus malesuada lectus ut egestas. Phasellus finibus sodales + nunc vel blandit. In tellus augue, posuere non libero eget, rhoncus + tempus dui. Proin consequat libero ac felis volutpat, nec tempor ipsum + dapibus. In vitae arcu efficitur, venenatis ipsum sit amet, tempus + massa. + + + ); +} diff --git a/packages/web/src/components/TwoColContent/TwoColContent.tsx b/packages/web/src/components/TwoColContent/TwoColContent.tsx new file mode 100644 index 00000000..bef7dbea --- /dev/null +++ b/packages/web/src/components/TwoColContent/TwoColContent.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { makeStyles, createStyles } from "@material-ui/core/styles"; +import clsx from "clsx"; +import { CustomTheme } from "../../mui"; + +const useStyles = makeStyles((theme: CustomTheme) => + createStyles({ + root: { + display: "flex" + }, + layoutLeftCol: { + flexGrow: 0.5, + [theme.breakpoints.down("md")]: { + display: "none" + } + }, + layoutCenterCol: { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + flexGrow: 1, + overflowWrap: "anywhere", + [theme.breakpoints.up("md")]: { + flexGrow: 0, + flexShrink: 0, + flexBasis: theme.custom.layout.centerColBasis, + paddingLeft: theme.custom.layout.centerColPadding, + paddingRight: theme.custom.layout.centerColPadding + }, + "& img": { + maxWidth: "100%" + } + }, + layoutRightCol: { + flexGrow: 1, + flexBasis: theme.custom.layout.rightColBasis, + [theme.breakpoints.down("md")]: { + display: "none" + } + }, + rightCol: { + position: "sticky", + top: theme.spacing(14), // TODO: calculate it based on AdrBrowserLayout's topSpace var + alignSelf: "flex-start", + paddingLeft: theme.spacing(2), + minWidth: "20ch" + } + }) +); + +type TwoColContentProps = { + className?: string; + children: React.ReactNode; + rightColContent?: React.ReactNode; +}; + +export function TwoColContent({ + className, + children, + rightColContent +}: TwoColContentProps) { + const classes = useStyles(); + + return ( +
    +
    +
    {children}
    +
    + {rightColContent} +
    +
    + ); +} diff --git a/packages/web/src/components/TwoColContent/index.ts b/packages/web/src/components/TwoColContent/index.ts new file mode 100644 index 00000000..4f0a6258 --- /dev/null +++ b/packages/web/src/components/TwoColContent/index.ts @@ -0,0 +1 @@ +export * from "./TwoColContent"; diff --git a/packages/web/src/components/index.ts b/packages/web/src/components/index.ts new file mode 100644 index 00000000..149e4f4d --- /dev/null +++ b/packages/web/src/components/index.ts @@ -0,0 +1,6 @@ +export * from "./AdrStatusChip"; +export * from "./Markdown"; +export * from "./MarkdownHeading"; +export * from "./MarkdownToc"; +export * from "./SearchBox"; +export * from "./TwoColContent"; diff --git a/packages/web/src/contexts/AdrNavContext/AdrNavContext.ts b/packages/web/src/contexts/AdrNavContext/AdrNavContext.ts new file mode 100644 index 00000000..fb75195c --- /dev/null +++ b/packages/web/src/contexts/AdrNavContext/AdrNavContext.ts @@ -0,0 +1,9 @@ +import React from "react"; +import { AdrLight } from "../../types"; + +export type AdrNav = { + previousAdr?: AdrLight; + nextAdr?: AdrLight; +}; + +export const AdrNavContext = React.createContext({}); diff --git a/packages/web/src/contexts/AdrNavContext/index.ts b/packages/web/src/contexts/AdrNavContext/index.ts new file mode 100644 index 00000000..7d5f6258 --- /dev/null +++ b/packages/web/src/contexts/AdrNavContext/index.ts @@ -0,0 +1 @@ +export * from "./AdrNavContext"; diff --git a/packages/web/src/contexts/Log4brainsModeContext/Log4brainsModeContext.ts b/packages/web/src/contexts/Log4brainsModeContext/Log4brainsModeContext.ts new file mode 100644 index 00000000..d927b1aa --- /dev/null +++ b/packages/web/src/contexts/Log4brainsModeContext/Log4brainsModeContext.ts @@ -0,0 +1,8 @@ +import React from "react"; + +export enum Log4brainsMode { + preview = "preview", + static = "static" +} + +export const Log4brainsModeContext = React.createContext(Log4brainsMode.static); diff --git a/packages/web/src/contexts/Log4brainsModeContext/index.ts b/packages/web/src/contexts/Log4brainsModeContext/index.ts new file mode 100644 index 00000000..5a98bf0e --- /dev/null +++ b/packages/web/src/contexts/Log4brainsModeContext/index.ts @@ -0,0 +1 @@ +export * from "./Log4brainsModeContext"; diff --git a/packages/web/src/contexts/index.ts b/packages/web/src/contexts/index.ts new file mode 100644 index 00000000..68dcf830 --- /dev/null +++ b/packages/web/src/contexts/index.ts @@ -0,0 +1,2 @@ +export * from "./AdrNavContext"; +export * from "./Log4brainsModeContext"; diff --git a/packages/web/src/layouts/AdrBrowserLayout/AdrBrowserLayout.stories.tsx b/packages/web/src/layouts/AdrBrowserLayout/AdrBrowserLayout.stories.tsx new file mode 100644 index 00000000..917f992f --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/AdrBrowserLayout.stories.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Meta, Story } from "@storybook/react"; +import { AdrBrowserLayout, AdrBrowserLayoutProps } from ".."; +import { adrMocks } from "../../../.storybook/mocks"; +import { toAdrLight } from "../../types"; + +const Template: Story = (args) => ( + +); + +export default { + title: "Layouts/AdrBrowser", + component: AdrBrowserLayout +} as Meta; + +export const Default = Template.bind({}); +Default.args = { adrs: adrMocks.map(toAdrLight) }; + +export const LoadingMenu = Template.bind({}); +LoadingMenu.args = {}; + +export const ReloadingMenu = Template.bind({}); +ReloadingMenu.args = { adrs: adrMocks.map(toAdrLight), adrsReloading: true }; + +export const EmptyMenu = Template.bind({}); +EmptyMenu.args = { adrs: [] }; + +export const RoutingProgressBar = Template.bind({}); +RoutingProgressBar.args = { adrs: adrMocks.map(toAdrLight), routing: true }; diff --git a/packages/web/src/layouts/AdrBrowserLayout/AdrBrowserLayout.tsx b/packages/web/src/layouts/AdrBrowserLayout/AdrBrowserLayout.tsx new file mode 100644 index 00000000..96275b16 --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/AdrBrowserLayout.tsx @@ -0,0 +1,485 @@ +import React from "react"; +import { + AppBar, + // Divider, + Drawer, + List, + // ListItem, + // ListItemIcon, + // ListItemText, + Toolbar, + Link as MuiLink, + Typography, + Backdrop, + NoSsr, + CircularProgress, + Grow, + Fade, + Hidden, + IconButton +} from "@material-ui/core"; +import { Menu as MenuIcon, Close as CloseIcon } from "@material-ui/icons"; +import { createStyles, makeStyles } from "@material-ui/core/styles"; +// import { +// ChevronRight as ChevronRightIcon, +// PlaylistAddCheck as PlaylistAddCheckIcon +// } from "@material-ui/icons"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import clsx from "clsx"; +import { AdrMenu } from "./components/AdrMenu"; +import { CustomTheme } from "../../mui"; +import { ConnectedSearchBox } from "./components/ConnectedSearchBox/ConnectedSearchBox"; +import { AdrLight } from "../../types"; +import { AdrNav, AdrNavContext } from "../../contexts"; +import { RoutingProgress } from "./components/RoutingProgress"; + +const drawerWidth = 380; +const searchTransitionDuration = 300; + +const useStyles = makeStyles((theme: CustomTheme) => { + const topSpace = theme.spacing(6); + return createStyles({ + root: { + display: "flex" + }, + layoutLeftCol: { + flexGrow: 0.5, + [theme.breakpoints.down("md")]: { + display: "none" + } + }, + layoutCenterCol: { + paddingLeft: theme.custom.layout.centerColPadding, + paddingRight: theme.custom.layout.centerColPadding, + flexGrow: 1, + [theme.breakpoints.up("md")]: { + flexGrow: 0, + flexShrink: 0, + flexBasis: theme.custom.layout.centerColBasis + } + }, + layoutRightCol: { + flexGrow: 1, + flexBasis: theme.custom.layout.rightColBasis, + [theme.breakpoints.down("md")]: { + display: "none" + } + }, + appBar: { + zIndex: theme.zIndex.drawer + 1 + }, + appBarMenuButton: { + [theme.breakpoints.up("sm")]: { + display: "none" + } + }, + appBarTitle: { + display: "none", + [theme.breakpoints.up("sm")]: { + display: "flex", + alignItems: "center", + width: drawerWidth - theme.spacing(3), + flexGrow: 0, + flexShrink: 0, + cursor: "pointer" + } + }, + appBarTitleLink: { + display: "block", + color: "inherit", + "&:hover": { + color: "inherit" + }, + marginLeft: theme.spacing(2) + }, + searchBackdrop: { + zIndex: theme.zIndex.modal - 2 + }, + searchBox: { + zIndex: theme.zIndex.modal - 1, + width: "100%", + [theme.breakpoints.up("md")]: { + width: "70%" + }, + transition: theme.transitions.create("width", { + duration: searchTransitionDuration + }) + }, + searchBoxOpen: { + width: "100%" + }, + drawer: { + [theme.breakpoints.up("sm")]: { + width: drawerWidth, + flexShrink: 0 + } + }, + drawerPaper: { + width: drawerWidth + }, + drawerContainer: { + height: "100%", + display: "flex", + flexDirection: "column", + [theme.breakpoints.up("sm")]: { + paddingTop: topSpace + } + }, + drawerToolbar: { + visibility: "visible", + [theme.breakpoints.up("sm")]: { + visibility: "hidden" + }, + justifyContent: "space-between" + }, + adrMenu: { + flexGrow: 1, + flexShrink: 1, + overflow: "auto", + "&::-webkit-scrollbar": { + width: 6, + backgroundColor: theme.palette.background + }, + "&::-webkit-scrollbar-thumb": { + borderRadius: 10, + "-webkit-box-shadow": "inset 0 0 2px rgba(0,0,0,.3)", + backgroundColor: theme.palette.grey[400] + } + }, + bottomMenuList: { + flexGrow: 0, + flexShrink: 0 + }, + adlTitleAndSpinner: { + display: "flex", + justifyContent: "space-between", + paddingLeft: theme.spacing(2), + [theme.breakpoints.up("sm")]: { + paddingLeft: theme.spacing(3) + }, + paddingBottom: theme.spacing(0.5), + paddingRight: theme.spacing(3) + }, + adlTitle: { + fontWeight: theme.typography.fontWeightBold + }, + adrMenuSpinner: { + alignSelf: "center", + marginTop: "30vh" + }, + container: { + flexGrow: 1, + paddingTop: theme.spacing(2), + [theme.breakpoints.up("sm")]: { + paddingTop: topSpace + } + }, + content: { + minHeight: `calc(100vh - 35px - ${ + theme.spacing(1) + theme.spacing(8) + }px)`, // TODO: calc AppBar height more precisely + [theme.breakpoints.up("sm")]: { + minHeight: `calc(100vh - 35px - ${topSpace + theme.spacing(8)}px)` // TODO: calc AppBar height more precisely + } + }, + footer: { + backgroundColor: theme.palette.grey[100], + color: theme.palette.grey[500], + height: 35, + display: "flex", + marginTop: theme.spacing(6) + }, + footerText: { + fontSize: "0.77rem" + }, + footerLink: { + color: theme.palette.grey[600], + fontSize: "0.8rem", + "&:hover": { + color: theme.palette.grey[800] + } + }, + footerContent: { + display: "flex", + flexDirection: "column", + justifyContent: "center" + } + }); +}); + +function buildAdrNav(currentAdr: AdrLight, adrs: AdrLight[]): AdrNav { + const currentIndex = adrs + .map((adr, index) => (adr.slug === currentAdr.slug ? index : undefined)) + .filter((adr) => adr !== undefined) + .pop(); + const previousAdr = + currentIndex !== undefined && currentIndex < adrs.length - 1 + ? adrs[currentIndex + 1] + : undefined; + const nextAdr = + currentIndex !== undefined && currentIndex > 0 + ? adrs[currentIndex - 1] + : undefined; + return { + previousAdr, + nextAdr + }; +} + +export type AdrBrowserLayoutProps = { + projectName: string; + adrs?: AdrLight[]; // undefined -> loading, empty -> empty + adrsReloading?: boolean; + currentAdr?: AdrLight; + children: React.ReactNode; + routing?: boolean; + l4bVersion: string; +}; + +export function AdrBrowserLayout({ + projectName, + adrs, + adrsReloading = false, + currentAdr, + children, + routing = false, + l4bVersion +}: AdrBrowserLayoutProps) { + const classes = useStyles(); + const router = useRouter(); + + const [mobileDrawerOpen, setMobileDrawerOpen] = React.useState(false); + + const handleMobileDrawerToggle = () => { + setMobileDrawerOpen(!mobileDrawerOpen); + }; + + React.useEffect(() => { + const closeMobileDrawer = () => setMobileDrawerOpen(false); + router?.events.on("routeChangeStart", closeMobileDrawer); + return () => { + router?.events.off("routeChangeStart", closeMobileDrawer); + }; + }, [router]); + + const [searchOpen, setSearchOpenState] = React.useState(false); + const [searchReallyOpen, setSearchReallyOpenState] = React.useState(false); + + const drawer = ( +
    + +
    + + + Log4brains logo + + + + + + + +
    + + Decision log + + + + + +
    + + + + + + {adrs === undefined && ( + + )} + + + {/* + + + + + + + Filters + + + */} + {/* + + + + + + + + */} + +
    + ); + + return ( +
    + + {routing && } + + + + + +
    +
    + Log4brains logo +
    +
    + + + {projectName} + + + + + Architecture knowledge base + + +
    +
    + +
    +
    + + + { + setSearchOpenState(true); + // Delayed real opening because otherwise the dropdown width is bugged + setTimeout( + () => setSearchReallyOpenState(true), + searchTransitionDuration + 100 + ); + }} + onClose={() => { + setSearchOpenState(false); + setSearchReallyOpenState(false); + }} + open={searchReallyOpen} + className={clsx(classes.searchBox, { + [classes.searchBoxOpen]: searchOpen + })} + /> + +
    +
    + + + + + +
    + +
    + + {children} + +
    +
    +
    +
    + + Powered by{" "} + + Log4brains + {" "} + + {l4bVersion ? `(v${l4bVersion})` : null} + + +
    +
    +
    +
    +
    + ); +} diff --git a/packages/web/src/layouts/AdrBrowserLayout/ConnectedAdrBrowserLayout.tsx b/packages/web/src/layouts/AdrBrowserLayout/ConnectedAdrBrowserLayout.tsx new file mode 100644 index 00000000..4014dbb4 --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/ConnectedAdrBrowserLayout.tsx @@ -0,0 +1,184 @@ +import React from "react"; +import Router, { useRouter } from "next/router"; +// import io from "socket.io-client"; // loaded by _document.tsx so that we don't add this lib in the static mode bundle +import type { FileWatcherEvent } from "@log4brains/core"; +import { Adr, AdrLight } from "../../types"; +import { Log4brainsMode, Log4brainsModeContext } from "../../contexts"; +import { AdrBrowserLayout, AdrBrowserLayoutProps } from "./AdrBrowserLayout"; +// eslint-disable-next-line import/no-cycle +import { + AdrScene, + AdrSceneProps, + IndexScene, + IndexSceneProps +} from "../../scenes"; +import { debug } from "../../lib/debug"; + +function isReactElement( + component: React.ReactNode +): component is React.ReactElement { + return ( + !!component && + typeof component === "object" && + "type" in component && + "props" in component + ); +} + +function isAdrSceneChild( + component: React.ReactNode +): component is React.ReactElement { + return isReactElement(component) && component.type === AdrScene; +} + +function isIndexSceneChild( + component: React.ReactNode +): component is React.ReactElement { + return isReactElement(component) && component.type === IndexScene; +} + +function hasAdrMetadataChanged(previous: Adr, current: Adr): boolean { + return ( + previous.title !== current.title || + previous.status !== current.status || + previous.package !== current.package || + previous.publicationDate !== current.publicationDate + ); +} + +async function hotReloadCurrentPage(): Promise { + /** + * #NEXTJS-HACK + * We clear Next.JS Router's "static data cache" to make our Hot Reload feature work. + * In fact, we trigger a page re-render every time an ADR changes and we absolutely need up-to-date data on every render. + * So we force a new request to the server. + */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + Router.router.sdc = {}; + + await Router.replace(window.location.href); +} + +type ConnectedAdrBrowserLayoutProps = Omit< + AdrBrowserLayoutProps, + "adrs" | "adrsReloading" | "routing" +> & { + // Defined for IndexScene to speed up the 1st load and for SEO. Not defined for AdrScenes to avoid a full-rebuild for each change. + // Will load them asynchronously if undefined + adrs?: AdrLight[]; +}; + +// eslint-disable-next-line sonarjs/cognitive-complexity +export function ConnectedAdrBrowserLayout( + props: ConnectedAdrBrowserLayoutProps +) { + const { adrs: preloadedAdrs } = props; + + const router = useRouter(); + const mode = React.useContext(Log4brainsModeContext); + const [adrs, setAdrsState] = React.useState( + preloadedAdrs ? [...preloadedAdrs].reverse() : preloadedAdrs + ); + const [adrsLoading, setAdrsLoadingState] = React.useState(false); + const [routing, setRoutingState] = React.useState(false); + + const previousProps = React.useRef( + null + ); + const latestProps = React.useRef(props); + React.useEffect(() => { + previousProps.current = latestProps.current; + latestProps.current = props; + }); + + // ADRs list for the navigation + const updateAdrsList = React.useCallback(async () => { + setAdrsLoadingState(true); + const adrsRes = (await ( + await fetch( + mode === Log4brainsMode.preview + ? `/api/adr` + : `${router.basePath}/data/${process.env.NEXT_BUILD_ID}/adrs.json` + ) + ).json()) as AdrLight[]; + adrsRes.reverse(); // @see Log4brains.searchAdrs(): they are returned by chronological order ASC. We display them DESC in the UI + setAdrsState(adrsRes); + setAdrsLoadingState(false); + }, [mode, router.basePath]); + + React.useEffect(() => { + if (!adrs) { + void updateAdrsList(); + } + }, [updateAdrsList, adrs]); + + // Routing progress bar + Router.events.on("routeChangeStart", () => setRoutingState(true)); + Router.events.on("routeChangeComplete", () => setRoutingState(false)); + Router.events.on("routeChangeError", () => setRoutingState(false)); // TODO: show a modal? + + // Hot Reload + React.useEffect(() => { + if (mode !== Log4brainsMode.preview || window.io === undefined) { + return () => {}; + } + + const socket = io(); + socket.on("FileWatcher", async (event: FileWatcherEvent) => { + debug(`[FileWatcher] ${event.type} - ${event.relativePath}`); + + const child = React.Children.only(latestProps.current.children); + const isMdFile = event.relativePath.toLowerCase().endsWith(".md"); + const isIndexFile = event.relativePath.toLowerCase().endsWith("index.md"); + + // * HOT RELOAD + // - ADR page && current ADR file changed + // - Index page && index.md changed + const needsHotReload = + (isAdrSceneChild(child) && + child.props.currentAdr.file.relativePath.toLowerCase() === + event.relativePath.toLowerCase()) || + (isIndexSceneChild(child) && isIndexFile); + if (needsHotReload) { + await hotReloadCurrentPage(); + } + + // * ADR LIST UPDATE (for menu and nav) + // - If any .md file changed, except: + // - If it's index.md + // - If the current ADR changed (ie a Hot Reload was triggered) BUT not its metadata (title, status, date...) [for perf. reasons] + const previousChild = previousProps.current + ? React.Children.only(previousProps.current.children) + : undefined; + const currentMetadataChanged = + isAdrSceneChild(child) && + previousChild && + isAdrSceneChild(previousChild) && + hasAdrMetadataChanged( + child.props.currentAdr, + previousChild.props.currentAdr + ); + if ( + isMdFile && + !isIndexFile && + (!needsHotReload || currentMetadataChanged) + ) { + await updateAdrsList(); + } + }); + + return () => { + socket.disconnect(); + }; + }, [mode, updateAdrsList]); + + return ( + + ); +} diff --git a/packages/web/src/layouts/AdrBrowserLayout/components/AdrMenu/AdrMenu.tsx b/packages/web/src/layouts/AdrBrowserLayout/components/AdrMenu/AdrMenu.tsx new file mode 100644 index 00000000..42516b79 --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/components/AdrMenu/AdrMenu.tsx @@ -0,0 +1,214 @@ +import React from "react"; +import moment from "moment"; +import { Typography, Link as MuiLink } from "@material-ui/core"; +import { + Timeline, + TimelineConnector, + TimelineContent, + TimelineDot, + TimelineItem, + TimelineOppositeContent, + TimelineSeparator +} from "@material-ui/lab"; +import { createStyles, Theme, makeStyles } from "@material-ui/core/styles"; +import { + EmojiFlags as EmojiFlagsIcon, + CropFree as CropFreeIcon +} from "@material-ui/icons"; +import Link from "next/link"; +import clsx from "clsx"; +import { AdrStatusChip } from "../../../../components"; +import { AdrLight } from "../../../../types"; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: {}, + emptyLabel: { + color: theme.palette.grey[500], + marginTop: theme.spacing(3), + marginLeft: "6ch" + }, + timeline: { + padding: 0 + }, + adrLink: { + display: "block" + }, + timelineItem: { + "&:hover": { + "& $timelineConnector": { + backgroundColor: theme.palette.primary.main + } + } + }, + selectedTimelineItem: { + "&:hover": { + "& $timelineConnector": { + backgroundColor: theme.palette.secondary.main + } + }, + "& $timelineConnector": { + backgroundColor: theme.palette.secondary.main + }, + "& $adrLink": { + color: theme.palette.secondary.main, + "&:hover": { + color: theme.palette.secondary.main + } + } + }, + timelineOppositeContentRoot: { + flex: "0 0 12ch" + }, + date: { + fontSize: "0.8rem", + color: theme.palette.grey[500] + }, + adrStatusChip: { + marginLeft: "-1ch" + }, + icon: { + verticalAlign: "middle" + }, + adrTitle: { + marginRight: "0.5ch" + }, + package: { + fontSize: "0.8rem", + verticalAlign: "text-top", + whiteSpace: "pre", + color: theme.palette.grey[700] + }, + timelineStartOppositeContentRoot: { + flex: "0 0 calc(12ch - 12px)" + }, + timelineContentContainer: { + paddingBottom: theme.spacing(2) + }, + timelineConnector: {}, + currentAdrTimelineConnector: { + backgroundColor: theme.palette.secondary.main + }, + // TODO: refactor with AdrLink.tsx + draftLink: {}, + proposedLink: {}, + acceptedLink: {}, + rejectedLink: { + textDecoration: "line-through" + }, + deprecatedLink: { + textDecoration: "line-through" + }, + supersededLink: { + textDecoration: "line-through" + } + }) +); + +type Props = { + adrs?: AdrLight[]; + currentAdrSlug?: string; + className?: string; +}; + +export function AdrMenu({ adrs, currentAdrSlug, className, ...props }: Props) { + const classes = useStyles(); + + if (adrs === undefined) { + return null; // Because inside a + } + + let lastDateString = ""; + + return ( +
    + {adrs.length === 0 && ( + + No ADR found :-( + + )} + + + {adrs.map((adr) => { + const currentDateString = moment( + adr.publicationDate || adr.creationDate + ).format("MMMM|YYYY"); + const dateString = + currentDateString === lastDateString ? "" : currentDateString; + lastDateString = currentDateString; + const [month, year] = dateString.split("|"); + + return ( + + + + {month} + + + {year} + + + + + + + +
    + + + + {adr.title || "Untitled"} + + {adr.package ? ( + + {" "} + {adr.package} + + ) : null} + + +
    + +
    +
    +
    +
    + ); + })} + + + + + + + + + + + +
    +
    + ); +} diff --git a/packages/web/src/layouts/AdrBrowserLayout/components/AdrMenu/index.ts b/packages/web/src/layouts/AdrBrowserLayout/components/AdrMenu/index.ts new file mode 100644 index 00000000..f655fa76 --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/components/AdrMenu/index.ts @@ -0,0 +1 @@ +export * from "./AdrMenu"; diff --git a/packages/web/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/ConnectedSearchBox.tsx b/packages/web/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/ConnectedSearchBox.tsx new file mode 100644 index 00000000..a35cb041 --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/ConnectedSearchBox.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { SearchBox, SearchBoxProps } from "../../../../components/SearchBox"; +import { + createSearchInstance, + Search, + SearchResult +} from "../../../../lib/search"; +import { Log4brainsMode, Log4brainsModeContext } from "../../../../contexts"; + +export type ConnectedSearchBoxProps = Omit< + SearchBoxProps, + "onQueryChange" | "query" | "results" | "onFocus" +>; + +export function ConnectedSearchBox(props: ConnectedSearchBoxProps) { + const mode = React.useContext(Log4brainsModeContext); + + const [searchInstance, setSearchInstance] = React.useState(); + const [pendingSearch, setPendingSearchState] = React.useState(false); + const [searchQuery, setSearchQueryState] = React.useState(""); + const [searchResults, setSearchResultsState] = React.useState( + [] + ); + + const handleSearchQueryChange = (query: string): void => { + setSearchQueryState(query); + + if (query.trim() === "") { + setSearchResultsState([]); + return; + } + + if (searchInstance) { + setSearchResultsState(searchInstance.search(query)); + if (pendingSearch) { + setPendingSearchState(false); + } + } else { + setPendingSearchState(true); + } + }; + + const handleFocus = async () => { + // We re-create the search instance on each focus in preview mode + if (!searchInstance || mode === Log4brainsMode.preview) { + setSearchInstance(await createSearchInstance(mode)); + } + }; + + // Trigger a possible pending search after setting the search instance + if (pendingSearch && searchInstance) { + handleSearchQueryChange(searchQuery); + } + + return ( + handleSearchQueryChange(query)} + query={searchQuery} + results={searchResults} + onFocus={handleFocus} + loading={pendingSearch} + /> + ); +} diff --git a/packages/web/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/index.ts b/packages/web/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/index.ts new file mode 100644 index 00000000..369d8e14 --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/index.ts @@ -0,0 +1 @@ +export * from "./ConnectedSearchBox"; diff --git a/packages/web/src/layouts/AdrBrowserLayout/components/RoutingProgress/RoutingProgress.tsx b/packages/web/src/layouts/AdrBrowserLayout/components/RoutingProgress/RoutingProgress.tsx new file mode 100644 index 00000000..97dcb544 --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/components/RoutingProgress/RoutingProgress.tsx @@ -0,0 +1,44 @@ +import { LinearProgress } from "@material-ui/core"; +import { makeStyles, createStyles } from "@material-ui/core/styles"; +import React from "react"; + +const useStyles = makeStyles(() => + createStyles({ + root: { + top: 0, + width: "100%", + height: 2, + position: "absolute" + } + }) +); + +export function RoutingProgress() { + const classes = useStyles(); + const [progress, setProgress] = React.useState(0); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((oldProgress) => { + if (oldProgress > 99.999) { + clearInterval(timer); + return 100; + } + return oldProgress + (100 - oldProgress) / 8; + }); + }, 100); + + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + ); +} diff --git a/packages/web/src/layouts/AdrBrowserLayout/components/RoutingProgress/index.ts b/packages/web/src/layouts/AdrBrowserLayout/components/RoutingProgress/index.ts new file mode 100644 index 00000000..59ac133f --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/components/RoutingProgress/index.ts @@ -0,0 +1 @@ +export * from "./RoutingProgress"; diff --git a/packages/web/src/layouts/AdrBrowserLayout/components/index.ts b/packages/web/src/layouts/AdrBrowserLayout/components/index.ts new file mode 100644 index 00000000..85cd5c30 --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/components/index.ts @@ -0,0 +1,2 @@ +export * from "./AdrMenu"; +export * from "./ConnectedSearchBox"; diff --git a/packages/web/src/layouts/AdrBrowserLayout/index.ts b/packages/web/src/layouts/AdrBrowserLayout/index.ts new file mode 100644 index 00000000..f3dde27b --- /dev/null +++ b/packages/web/src/layouts/AdrBrowserLayout/index.ts @@ -0,0 +1,3 @@ +export * from "./AdrBrowserLayout"; +// eslint-disable-next-line import/no-cycle +export * from "./ConnectedAdrBrowserLayout"; diff --git a/packages/web/src/layouts/index.ts b/packages/web/src/layouts/index.ts new file mode 100644 index 00000000..b2bffe93 --- /dev/null +++ b/packages/web/src/layouts/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/no-cycle +export * from "./AdrBrowserLayout"; diff --git a/packages/web/src/lib/adr-utils.ts b/packages/web/src/lib/adr-utils.ts new file mode 100644 index 00000000..2218f256 --- /dev/null +++ b/packages/web/src/lib/adr-utils.ts @@ -0,0 +1,12 @@ +import { Adr, AdrLight } from "../types"; + +export function getAdrBySlug( + slug: string, + adrs: AdrLight[] +): AdrLight | undefined { + return adrs.filter((a) => a.slug === slug).pop(); +} + +export function buildAdrUrl(adr: AdrLight | Adr): string { + return `/adr/${adr.slug}`; +} diff --git a/packages/web/src/lib/console.ts b/packages/web/src/lib/console.ts new file mode 100644 index 00000000..35fb56f4 --- /dev/null +++ b/packages/web/src/lib/console.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import chalk from "chalk"; +import { AppConsole, ConsoleCapturer } from "@log4brains/cli-common"; + +const debug = !!process.env.DEBUG; +const dev = process.env.NODE_ENV === "development"; + +export const appConsole = new AppConsole({ debug, traces: debug || dev }); + +/** + * #NEXTJS-HACK + * We want to hide the output of Next.js when we execute CLI commands. + * + * @param fn The code which calls Next.js methods for which we want to capture the output + */ +export async function execNext(fn: () => Promise): Promise { + const capturer = new ConsoleCapturer(); + if (debug) { + capturer.onLog = (method, args) => { + capturer.doPrintln(...["[Next] ", ...args].map((a) => chalk.dim(a))); + }; + } + capturer.start(); + await fn(); + capturer.stop(); +} diff --git a/packages/web/src/lib/core-api/getIndexPageMarkdown.ts b/packages/web/src/lib/core-api/getIndexPageMarkdown.ts new file mode 100644 index 00000000..2a3040f9 --- /dev/null +++ b/packages/web/src/lib/core-api/getIndexPageMarkdown.ts @@ -0,0 +1,23 @@ +import path from "path"; +import { promises as fsP } from "fs"; +import { getLog4brainsInstance } from "./instance"; + +export async function getIndexPageMarkdown(): Promise { + const instance = getLog4brainsInstance(); + const indexPath = path.join( + instance.workdir, + instance.config.project.adrFolder, + "index.md" + ); + + try { + return await fsP.readFile(indexPath, { + encoding: "utf8" + }); + } catch (e) { + return `# Architecture knowledge base + +Please create \`${instance.config.project.adrFolder}/index.md\` to customize this homepage. +`; + } +} diff --git a/packages/web/src/lib/core-api/index.ts b/packages/web/src/lib/core-api/index.ts new file mode 100644 index 00000000..4e049273 --- /dev/null +++ b/packages/web/src/lib/core-api/index.ts @@ -0,0 +1,2 @@ +export * from "./instance"; +export * from "./getIndexPageMarkdown"; diff --git a/packages/web/src/lib/core-api/instance.ts b/packages/web/src/lib/core-api/instance.ts new file mode 100644 index 00000000..e6d93498 --- /dev/null +++ b/packages/web/src/lib/core-api/instance.ts @@ -0,0 +1,22 @@ +import path from "path"; +import { Log4brains } from "@log4brains/core"; +import { getConfig } from "../next"; + +let instance: Log4brains; + +export function getLog4brainsInstance(): Log4brains { + if (!instance) { + if (process.env.LOG4BRAINS_PHASE === "initial-build") { + // Noop instance during "next build" phase + instance = Log4brains.create( + path.join( + getConfig().serverRuntimeConfig.PROJECT_ROOT, + "lib/core-api/noop" + ) + ); + } else { + instance = Log4brains.create(process.env.LOG4BRAINS_CWD || "."); + } + } + return instance; +} diff --git a/packages/web/src/lib/core-api/noop/.log4brains.yml b/packages/web/src/lib/core-api/noop/.log4brains.yml new file mode 100644 index 00000000..52d0f439 --- /dev/null +++ b/packages/web/src/lib/core-api/noop/.log4brains.yml @@ -0,0 +1,7 @@ +--- +# This config file is used by instance.ts during Next.js build phase, +# When we want to create a noop instance of Log4brains +project: + name: noop + tz: Etc/UTC + adrFolder: ./noop-adrs diff --git a/packages/web/src/lib/core-api/noop/noop-adrs/.gitignore b/packages/web/src/lib/core-api/noop/noop-adrs/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/packages/web/src/lib/debug.ts b/packages/web/src/lib/debug.ts new file mode 100644 index 00000000..09a79caa --- /dev/null +++ b/packages/web/src/lib/debug.ts @@ -0,0 +1,7 @@ +/* eslint-disable no-console */ + +export function debug(message: string): void { + if (process.env.NODE_ENV === "development") { + console.log(message); + } +} diff --git a/packages/web/src/lib/next.ts b/packages/web/src/lib/next.ts new file mode 100644 index 00000000..a1a1a153 --- /dev/null +++ b/packages/web/src/lib/next.ts @@ -0,0 +1,40 @@ +import path from "path"; +import getNextConfig from "next/config"; + +export function getNextJsDir(): string { + // When built, there is no more "src/" directory + return path.resolve(path.join(__dirname, "..")); +} + +export type L4bNextConfig = { + serverRuntimeConfig: { + PROJECT_ROOT: string; + VERSION: string; + }; +}; + +function isObjectWithGivenProperties( + obj: unknown, + properties: K[] +): obj is Record { + return ( + typeof obj === "object" && + obj !== null && + properties.every((property) => property in obj) + ); +} + +function isL4bNextConfig(config: unknown): config is L4bNextConfig { + return ( + isObjectWithGivenProperties(config, ["serverRuntimeConfig"]) && + isObjectWithGivenProperties(config.serverRuntimeConfig, ["PROJECT_ROOT"]) + ); +} + +export function getConfig(): L4bNextConfig { + const config = getNextConfig() as unknown; + if (!isL4bNextConfig(config)) { + throw new Error(`Invalid Next.js config object: ${config}`); + } + return config; +} diff --git a/packages/web/src/lib/search/Search.ts b/packages/web/src/lib/search/Search.ts new file mode 100644 index 00000000..176849dd --- /dev/null +++ b/packages/web/src/lib/search/Search.ts @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { AdrDto } from "@log4brains/core"; +import lunr from "lunr"; + +type AdrForSearch = { + title: string; + verbatim: string; // body without Markdown or HTML tags, without recurring headers +}; + +export type SerializedIndex = { + lunr: object; + adrs: [string, AdrForSearch][]; +}; + +export type SearchResult = { + slug: string; + href: string; + title: string; + + /** + * A number between 0 and 1 representing how similar this document is to the query. + * @see lunr.Index.Result + */ + score: number; + + // TODO: add highlighted verbatim (https://github.com/thomvaill/log4brains/issues/5) +}; + +function mapToJson(map: Map): [K, V][] { + return Array.from(map.entries()); +} + +function mapFromJson(entries: [K, V][]): Map { + return new Map(entries); +} + +/** + * Inspired by https://github.com/squidfunk/mkdocs-material/tree/master/src/assets/javascripts/integrations/search + */ +export class Search { + private constructor( + private readonly index: lunr.Index, + private readonly adrs: Map + ) {} + + search(query: string): SearchResult[] { + return this.index.search(`${query}*`).map((result) => { + const adr = this.adrs.get(result.ref); + if (!adr) { + throw new Error(`Invalid Search instance: missing ADR "${result.ref}"`); + } + return { + slug: result.ref, + href: `/adr/${result.ref}`, + title: adr.title, + score: result.score + }; + }); + } + + serializeIndex(): SerializedIndex { + return { lunr: this.index.toJSON(), adrs: mapToJson(this.adrs) }; + } + + static createFromAdrs(adrs: AdrDto[]): Search { + const adrsForSearch = new Map( + adrs.map((adr) => [ + adr.slug, + { + title: adr.title || "Untitled", + verbatim: adr.body.enhancedMdx // TODO: remove tags (https://github.com/thomvaill/log4brains/issues/5) + } + ]) + ); + + const index = lunr((builder) => { + builder.ref("slug"); + builder.field("title", { boost: 1000 }); + builder.field("verbatim"); + // eslint-disable-next-line no-param-reassign + builder.metadataWhitelist = ["position"]; + + adrsForSearch.forEach((adr, slug) => { + builder.add({ + slug, + title: adr.title, + verbatim: adr.verbatim + }); + }); + }); + return new Search(index, adrsForSearch); + } + + static createFromSerializedIndex(serializedIndex: SerializedIndex): Search { + return new Search( + lunr.Index.load(serializedIndex.lunr), + mapFromJson(serializedIndex.adrs) + ); + } +} diff --git a/packages/web/src/lib/search/index.ts b/packages/web/src/lib/search/index.ts new file mode 100644 index 00000000..70434468 --- /dev/null +++ b/packages/web/src/lib/search/index.ts @@ -0,0 +1,2 @@ +export * from "./Search"; +export * from "./instance"; diff --git a/packages/web/src/lib/search/instance.ts b/packages/web/src/lib/search/instance.ts new file mode 100644 index 00000000..d006bfa4 --- /dev/null +++ b/packages/web/src/lib/search/instance.ts @@ -0,0 +1,29 @@ +import Router from "next/router"; +import { Log4brainsMode } from "../../contexts"; +import { Search, SerializedIndex } from "./Search"; + +function isSerializedIndex(obj: unknown): obj is SerializedIndex { + return ( + typeof obj === "object" && + obj !== null && + "lunr" in obj && + "adrs" in obj && + Array.isArray((obj as SerializedIndex).adrs) + ); +} + +export async function createSearchInstance( + mode: Log4brainsMode +): Promise { + const index = (await ( + await fetch( + mode === Log4brainsMode.preview + ? `/api/search-index` + : `${Router.basePath}/data/${process.env.NEXT_BUILD_ID}/search-index.json` + ) + ).json()) as unknown; + if (!isSerializedIndex(index)) { + throw new Error(`Invalid Search SerializedIndex: ${index}`); + } + return Search.createFromSerializedIndex(index); +} diff --git a/packages/web/src/lib/slugify.ts b/packages/web/src/lib/slugify.ts new file mode 100644 index 00000000..48732120 --- /dev/null +++ b/packages/web/src/lib/slugify.ts @@ -0,0 +1,9 @@ +import slugifyFn from "slugify"; + +// used to slugify markdown paragraph IDs +export function slugify(string: string): string { + return slugifyFn(string, { + lower: true, + strict: true + }); +} diff --git a/packages/web/src/lib/toc-utils/Toc.ts b/packages/web/src/lib/toc-utils/Toc.ts new file mode 100644 index 00000000..e513c80f --- /dev/null +++ b/packages/web/src/lib/toc-utils/Toc.ts @@ -0,0 +1,23 @@ +import { TocContainer } from "./TocContainer"; +import { TocSection } from "./TocSection"; + +export class Toc implements TocContainer { + public readonly parent = null; + + readonly children: TocSection[] = []; + + createChild(title: string, id: string): TocSection { + const child = new TocSection(this, title, id); + this.children.push(child); + return child; + } + + // eslint-disable-next-line class-methods-use-this + getLevel(): number { + return 0; + } + + render(renderer: (title: string, id: string, children: T[]) => T): T[] { + return this.children.map((child) => child.render(renderer)); + } +} diff --git a/packages/web/src/lib/toc-utils/TocBuilder.test.ts b/packages/web/src/lib/toc-utils/TocBuilder.test.ts new file mode 100644 index 00000000..d6801988 --- /dev/null +++ b/packages/web/src/lib/toc-utils/TocBuilder.test.ts @@ -0,0 +1,121 @@ +import { Toc } from "./Toc"; +import { TocSection } from "./TocSection"; +import { TocBuilder } from "./TocBuilder"; + +type TocRep = { + level: number; + title: string; + children: TocRep[]; +}; +const tocToArray = (section: TocSection | Toc): TocRep[] => { + return section.children.map((child) => { + return { + level: child.getLevel(), + title: child.title, + children: tocToArray(child) + }; + }); +}; + +describe("TocBuilder", () => { + it("should add sections to the TOC correctly", () => { + const builder = new TocBuilder(); + builder.addSection(1, "Header 1", "Header1"); + builder.addSection(2, "Header 1.1", "Header1.1"); + builder.addSection(3, "Header 1.1.1", "Header1.1.1"); + builder.addSection(4, "Header 1.1.1.1", "Header1.1.1.1"); + builder.addSection(5, "Header 1.1.1.1.1", "Header1.1.1.1.1"); + builder.addSection(6, "Header 1.1.1.1.1.1", "Header1.1.1.1.1.1"); + builder.addSection(2, "Header 1.2", "Header1.2"); + builder.addSection(3, "Header 1.2.1", "Header1.2.1"); + builder.addSection(3, "Header 1.2.2", "Header1.2.2"); + builder.addSection(3, "Header 1.2.3", "Header1.2.3"); + builder.addSection(1, "Header 2", "Header2"); + builder.addSection(1, "Header 3", "Header3"); + + const toc = builder.getToc(); + expect(tocToArray(toc)).toEqual([ + { + level: 1, + title: "Header 1", + children: [ + { + level: 2, + title: "Header 1.1", + children: [ + { + level: 3, + title: "Header 1.1.1", + children: [ + { + level: 4, + title: "Header 1.1.1.1", + children: [ + { + level: 5, + title: "Header 1.1.1.1.1", + children: [ + { + level: 6, + title: "Header 1.1.1.1.1.1", + children: [] + } + ] + } + ] + } + ] + } + ] + }, + { + level: 2, + title: "Header 1.2", + children: [ + { + level: 3, + title: "Header 1.2.1", + children: [] + }, + { + level: 3, + title: "Header 1.2.2", + children: [] + }, + { + level: 3, + title: "Header 1.2.3", + children: [] + } + ] + } + ] + }, + { + level: 1, + title: "Header 2", + children: [] + }, + { + level: 1, + title: "Header 3", + children: [] + } + ]); + }); + + it("debug", () => { + const builder = new TocBuilder(); + builder.addSection(1, "Header 1", "Header1"); + builder.addSection(2, "Header 1.1", "Header1.1"); + builder.addSection(3, "Header 1.1.1", "Header1.1.1"); + builder.addSection(4, "Header 1.1.1.1", "Header1.1.1.1"); + builder.addSection(1, "Header 2", "Header2"); + builder.addSection(1, "Header 3", "Header3"); + builder.addSection(2, "Header 3.1", "Header3.1"); + builder.addSection(2, "Header 3.2", "Header3.2"); + + const toc = builder.getToc(); + expect(toc.children.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/web/src/lib/toc-utils/TocBuilder.ts b/packages/web/src/lib/toc-utils/TocBuilder.ts new file mode 100644 index 00000000..72a7d23d --- /dev/null +++ b/packages/web/src/lib/toc-utils/TocBuilder.ts @@ -0,0 +1,40 @@ +import { Toc } from "./Toc"; +import { TocContainer } from "./TocContainer"; + +export class TocBuilder { + private readonly root: Toc; + + private current: TocContainer; + + constructor() { + this.root = new Toc(); + this.current = this.root; + } + + addSection(level: number, title: string, id: string): void { + if (level <= 0) { + throw new Error("Level must be > 0"); + } + + if (level < this.current.getLevel() + 1) { + // eg: section to add = H2, current section = H2 -> we have to step back from one level + if (!this.current.parent) { + throw new Error("Never happens thanks to recursion"); + } + this.current = this.current.parent; + this.addSection(level, title, id); + } else if (level > this.current.getLevel() + 1) { + // eg: section to add = H4, current section = H2 -> we have to create an empty intermediate section + this.current = this.current.createChild("", ""); + this.addSection(level, title, id); + } else if (level === this.current.getLevel() + 1) { + // recursion stop condition + // eg: section to add = H2, current section = H1 + this.current = this.current.createChild(title, id); + } + } + + getToc(): Toc { + return this.root; + } +} diff --git a/packages/web/src/lib/toc-utils/TocContainer.ts b/packages/web/src/lib/toc-utils/TocContainer.ts new file mode 100644 index 00000000..33df586d --- /dev/null +++ b/packages/web/src/lib/toc-utils/TocContainer.ts @@ -0,0 +1,5 @@ +export interface TocContainer { + parent: TocContainer | null; + getLevel(): number; + createChild(title: string, id: string): TocContainer; +} diff --git a/packages/web/src/lib/toc-utils/TocSection.ts b/packages/web/src/lib/toc-utils/TocSection.ts new file mode 100644 index 00000000..7c02e21b --- /dev/null +++ b/packages/web/src/lib/toc-utils/TocSection.ts @@ -0,0 +1,34 @@ +import { TocContainer } from "./TocContainer"; + +export class TocSection { + readonly children: TocSection[] = []; + + readonly parent: TocContainer; + + readonly title: string; + + readonly id: string; + + // Typescript parameter properties are not supported by Storybook for now! :-( + // https://github.com/storybookjs/storybook/issues/12019 + constructor(parent: TocContainer, title: string, id: string) { + this.parent = parent; + this.title = title; + this.id = id; + } + + createChild(title: string, id: string): TocSection { + const child = new TocSection(this, title, id); + this.children.push(child); + return child; + } + + getLevel(): number { + return this.parent.getLevel() + 1; + } + + render(renderer: (title: string, id: string, children: T[]) => T): T { + const c = this.children.map((child) => child.render(renderer)); + return renderer(this.title, this.id, c); + } +} diff --git a/packages/web/src/lib/toc-utils/index.ts b/packages/web/src/lib/toc-utils/index.ts new file mode 100644 index 00000000..1b7b0ff6 --- /dev/null +++ b/packages/web/src/lib/toc-utils/index.ts @@ -0,0 +1,4 @@ +export * from "./Toc"; +export * from "./TocBuilder"; +export * from "./TocContainer"; +export * from "./TocSection"; diff --git a/packages/web/src/mui/MuiDecorator.tsx b/packages/web/src/mui/MuiDecorator.tsx new file mode 100644 index 00000000..1f17c6a5 --- /dev/null +++ b/packages/web/src/mui/MuiDecorator.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { ThemeProvider } from "@material-ui/core/styles"; +import { CssBaseline } from "@material-ui/core"; +import { theme } from "./theme"; + +type Props = { + children: React.ReactNode; +}; + +export function MuiDecorator({ children }: Props) { + return ( + + + {children} + + ); +} diff --git a/packages/web/src/mui/index.ts b/packages/web/src/mui/index.ts new file mode 100644 index 00000000..63bf6bdd --- /dev/null +++ b/packages/web/src/mui/index.ts @@ -0,0 +1,2 @@ +export * from "./MuiDecorator"; +export * from "./theme"; diff --git a/packages/web/src/mui/theme.ts b/packages/web/src/mui/theme.ts new file mode 100644 index 00000000..74dc5d8d --- /dev/null +++ b/packages/web/src/mui/theme.ts @@ -0,0 +1,110 @@ +import { + createMuiTheme, + darken, + Theme, + responsiveFontSizes +} from "@material-ui/core/styles"; +import { red } from "@material-ui/core/colors"; + +export type CustomTheme = Theme & { + custom: { + layout: { + centerColBasis: number; + centerColPadding: number; + rightColBasis: number; + }; + }; +}; + +const primary = "#2176AE"; +const titleFontFamily = '"Roboto Slab", "Noto Serif", "Times New Roman", serif'; + +export const theme: CustomTheme = { + ...responsiveFontSizes( + createMuiTheme({ + palette: { + primary: { + main: primary + }, + secondary: { + main: "#FF007B" + }, + error: { + main: red.A400 + }, + background: { + default: "#fff" + } + }, + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + h1: { + fontFamily: titleFontFamily + }, + h2: { + fontFamily: titleFontFamily + }, + h3: { + fontFamily: titleFontFamily, + lineHeight: 1.1 + }, + h4: { + fontFamily: titleFontFamily + }, + h5: { + fontFamily: titleFontFamily + }, + h6: { + fontFamily: titleFontFamily + } + }, + props: { + MuiLink: { + underline: "none" + } + }, + overrides: { + MuiCssBaseline: { + "@global": { + html: { + maxWidth: "100%" + }, + body: { + padding: "0 !important", // for storybook + maxWidth: "100%" + }, + blockquote: { + margin: 0, + padding: "0 1em", + borderLeft: "0.25em solid #F8F8F8", + color: "#9e9e9e" + } + } + }, + MuiLink: { + root: { + "&:hover": { + color: darken(primary, 0.3) + } + } + } + }, + breakpoints: { + values: { + xs: 0, + sm: 900, + md: 1060, + lg: 1280, + xl: 1920 + } + } + }) + ), + custom: { + layout: { + centerColBasis: 750 + 4 * 8, + centerColPadding: 4 * 8, + rightColBasis: 180 + } + } +}; diff --git a/packages/web/src/pages/_app.tsx b/packages/web/src/pages/_app.tsx new file mode 100644 index 00000000..6f4eeed3 --- /dev/null +++ b/packages/web/src/pages/_app.tsx @@ -0,0 +1,62 @@ +import React, { useEffect } from "react"; +import Head from "next/head"; +import type { AppProps } from "next/app"; +import { useRouter } from "next/router"; +import "highlight.js/styles/github.css"; +import "../components/Markdown/hljs.css"; +import { NextComponentType, NextPageContext } from "next"; +import { MuiDecorator } from "../mui"; +import { Log4brainsMode, Log4brainsModeContext } from "../contexts"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ComponentWithLayout

    = NextComponentType & { + getLayout?: ( + page: JSX.Element, + layoutProps: Record + ) => JSX.Element; +}; +type AppPropsWithLayout

    > = AppProps

    & { + Component: ComponentWithLayout

    ; +}; + +export default function MyApp({ Component, pageProps }: AppPropsWithLayout) { + // Remove the server-side injected CSS (@see https://github.com/mui-org/material-ui/blob/master/examples/nextjs/pages/_app.js) + useEffect(() => { + const jssStyles = document.querySelector("#jss-server-side"); + jssStyles?.parentElement?.removeChild(jssStyles); + }); + + // Persistent Layout Pattern (https://adamwathan.me/2019/10/17/persistent-layout-patterns-in-nextjs/) + const getLayout = Component.getLayout || ((page) => page); + + const router = useRouter(); + const mode = process.env.NEXT_PUBLIC_LOG4BRAINS_STATIC + ? Log4brainsMode.static + : Log4brainsMode.preview; + + return ( + <> + + Architecture knowledge base + + + + + + + {getLayout(, pageProps)} + + + + ); +} diff --git a/packages/web/src/pages/_document.tsx b/packages/web/src/pages/_document.tsx new file mode 100644 index 00000000..5e5c706e --- /dev/null +++ b/packages/web/src/pages/_document.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import Document, { Html, Head, Main, NextScript } from "next/document"; +import { ServerStyleSheets } from "@material-ui/core/styles"; +import { theme } from "../mui"; + +// @see https://github.com/mui-org/material-ui/blob/master/examples/nextjs/pages/_document.js + +export default class MyDocument extends Document { + render() { + return ( + + + {/* PWA primary color */} + + + + + + +

    + + {!process.env.NEXT_PUBLIC_LOG4BRAINS_STATIC && ( +

    &B_mxw5p*g3OTlM3}1hMx$t5sBq`RiUzR;~{nf3@|cf$o^p>>)A7? z6HpoQPGwvl+jr%%`f1$xvk6v)RvcBocE*ggC*-^^OmlCEI^ah>SKc5Z133^Gq8iEb ztTSfxoo7quY67iSz|^O&SLd(#@f*Ekw5@lXJG*Y12S^iqELVhxyMdR~tc0|*4T=Fw z0$fUI@yxV>6w8<;M_2)M!JmH|Gm#nt)B>Ide<5^sz}o!H;Zbj-ve!BRK1jkpY~x5LKtB*d?y8gYSXagbCO{mgoc^t3tG_q& z*z5CqDnQPBPoKLYr;1{g;lE6Toe~unIsV((7`6Q~da+H|K%V zT{i{UGKUm>SJhK2f7)iiH;n<-qBK?G5SHXO7fO7MKAR%Fo3trH<4EB*Cu!#JO@%`J z(x{g{vt+Csha%F-5s_N#eRPv{D&e>*XUD4;XAkueUTwb%2k~G_OGY{pfT; ziy0F`LX{cGqXf{1l3&v**aA{BfyyZUkdw~QBaFtm_HaLpkf((|FA>+Q#Cl5mD!t$Y z{n(}y4xwhn%}iEfl@<(|+`OK*#aQJDV@RLCjZ4kM92MQI6{QR!V+ORCUvq(YNEdJ`uxpGmRIh1b&DsTZ6 zqJoJ?!P2t@_MriR&DJBSI)1M#Xy-KKqyiE!7;60j!n`qnKQRKUG(H%E&z*iReh9JZDgik1+$46j#1F-yv;FgqNnek}DR~7=z=nmCU zXbRYB2$1)@13(C_cm;!aO!jz0ZhUQSJcI}x^8;Z8RYHq}Yn??xXLY=&cR~+1 zo<)ed=pvz?DsfOMao8epG%#^IH*vBnae5_j<|1*9D(Ssc(xOGu5*O@0O&DrD%7ALZ z*6*9Jd%3|sUT*OJSdWTbl#~ob5&iAEfI9sn333}o`7R7Z36sDpG0vDfOT9Te8Q5zif8 z@R%TEx#7pw-hua1+b+~btvu%+B^2gKOH=fY`}A5&HSi?j)gBg!Xf}eKM3pJ-O)M#| z+TO|f##k!3Bm*@{h~JTf65>z)%NIMNeXzWJZ=sqJYQdkXeX!gVbWS#o*6BI#dHdnb zyVuXWFp!vM3|`WIyf(%eY4vA5pZ<)@fG8BpStX?~XrziwG};8GHNT5NLMT_fV;sZr zbz?J+=i|wUbT=g9j6Lv3_>uX|gx`AAELgb?{|o?w*N6tg=zN-$F(laLVS3hL>qBf- zWW>PmzNFn0FfAB5h|O`4@d2CU!Wf9p@}gZCFbQJPBz@prszO3K6Kx-8KYtR5iB3P@ zi2a^?otV^Mjb(yIlWBc6QARxNL(;n_6r|HI2}dhPJ+Ujr(qFMi%SIAW;}Np3FgvDa zoRw2%mhii#g0(2vU|2q2GDQv)Sz%S>*dmS~r&=S?;LsGH{zmbnbZ1AM@lZ7ng5_~S z38k=c!@wOlM8N$|#Ydxi)z&Eo2Lm@f3F?XAs>K;&8y!eOQt*_frpIqb&vr zh5umj17dlPH-8u+YO0$9Kj88K22x2<{#4|2GvI7v7M1i>oHxw(eahRGcrZ0oDEn(# zY^dG`j3;)IJ|!JPGse+yfsSLN_%xxPRm?iKy@SzB69%S5`!YREjzWGkw2s%k} zzDfObsFYl^nO?$k*^pUwJ%A^g3J`n2Ilgx3%n9$mEVG+f`>~59&IUb5XiG37Q~6)) zz4cd>``SIc=tU#l-3?MI-5@P3($bBjba%IubazQfcb9-P(jhG%EWS(KXP>)0W<|I1e&Ov&o#lX9I2Q)8c}%imt7>dhDZd=Yd-Ww z?^dZX{fp!jDd^ZmY3`K$hsB{-f-(Fb-Pwnz&KzTa*t_7-IRsl$peR5tDZY>FcG(gm z*(uF#@C{O2Fi@O*T#`;TP(qZR5!{XW@?|gID|CFQuV_sj;zs+&c)<}?&U6tEM6cIr*?VLv&N*cv#?1f5z^SbAQ>IRrA~nLFIASG$vN#j zBSvVTc%{dzFzpF3#0Q1XW!la4G4oG?W3aUb#N7c*!gGNk63(Te62;rzLJo1FXH=q!Qj8lDx z=#`TI@Y*p3^h9s+L_;}Sp4=Gu_uQFq<2{CXj$!b|rAd>LZa;pT1xgiqnci^D5gN%a zI+nT~Q3xGxI&Eaej=SERoGA5$CuLrtAZm$HuXfotW25h=2J9IqV|<96j_%obqk1f#`B9ymW_g0+$60S+vgFKV5K{^Q)#PPIkfNIVbds%pDV zTWVI{L4GaKmYazXg}NNP(Ot=B|9phKyq!uJt1I9g-BWmbJDrVIUnC{Juk!A8rbwag z{iB1eah@I+A|SnAXO0hrIC$yI{S^T}2HqFrc_$gWj}UWJA?&*~7>tSdMGR(4#3Bml z-ptFWxHC(0USqI)G4A^k{!ra?j)HRD+_zJui;SJO6NV6oFI%)G&saB&99?E%Ff=0Y8_ zted4Ie-1HSs2@bDOp78FMnom+Y(_LNnH3kAyDSS; zmn#!DTY)Tj#mjfK0QO>Wo97sF7g7ttUb$xM)Z5nCvFR&MiG4<+=_Wz{=G$}QSUdTh z%c9oz{_(Cak)Q`#iv$q30ab=bEI9Gqy%v+F2cuJk9MzZ56(bvgbjfrMfe4tF>FWab zk6aD-6)p}S0N_AO|BzSsZ~qDp1>D~}2T=P6kqYpgU<_1Ii1GIs-kf2Xj^xRpJ#Iev zBu+r+Efdq>J6$N%7xcCd_WQ1FbP^>fA>FE|MgbgFg&j&~IFYR)c@k^JG58`I)>|mn z?$Dqnbh=Q1z#XX1?1Nb#kia#Wc4^)X$bx3bSr-x0@+O2Wi1bM^2tlGy@VM4b5?A*A zQzHXMa)iMsUjRXQ$aMbp(+L7R{8yBxyrB4_9xDO3G1wPPE*6IJ#AF0~4C;^K!Wc^a z)?X2VU&Vz#A_V?OBtPr1{vGAjvTTIOMW*rw)qSNw+>gFj_6yOxp47D->KKh7RqVT9 ze>-uQTk-l+jZ-`ntRF2@TuR1sK4wiuQ;FSIi|Qfyx~+G;r!VZvI;6t08sK^ig6j$W z65e=j*hjpkF)7y^i+J-G9c@tuXps}p-$7lv2MX_Kt|#KwY%u#w0W9lKDL^1R&Zw(K zN1M|je>7Sz6X!i#?fBZOt{es&a*%Q9Eyd+F=vs0$7{ArDjmcE5)3I*(u;{ z7V(3H%~(4{M2maSU zL7othC&X|5gn$J{YX8BBf1)k{m=!~^=5mn1_{|3+3#dU1M=!dwCPV`yTffn$+6eOYg68j>{u0|siWsjcp5mY=m5eL`ir=G(?@Evj zyP&76l8W}1k7HEZ_cQUKmdquTpu{j&@kbB>dVo(SqLJ!7aBnHY9-;+ECve@<8ICZ% zf)aE+$rkY!Mt|5}#EDfZ?Z9|YxI2GVYxs`_**}Ms5Q1WmVg1dAb?F}z+0Q1x2UA>X zurnBjpb5-XkutsGYq$tfq}JlS(U3*25qA6gsr|w(ghiDGr9#YG@49ovC&U;Zcu29K zDNWRX#`+{u<3iG;tQ1$1zCWzy$HU#F&EO9Tnv-h@_yUcDHp41ft;xweH@`qI%*!_2UGIJLCe*|q+G6+#yih6A*D#Z^rgD}YZHtpfF6Jf zc@sn4#J~NU*d09ZD5vZG;4*(?M)4G-_aZ?Ec%l*4BEvoYlJ0V%4$9ey3?vVMRVo2! zg@g~n6ym2)h#9Ea#v#<5cHKe>&y?O=VATBaCVr~A(F2$hWGJPa^k#o5 z1}aRgO&Z6t8bHz15C`tj6E zZ(EmSPW;1}Qq7Pk`HHi%yz^ac?y3ypVtxH;X0TI&qsF{tV50_0>WroXQ3w=>`DhL1 zeV;WqEXBf)2|Y#pfXOxNq=;M3@``*tyDBFxzyxt9v6RbXEYWq=>qJR>XO&ckB<@g| znrSGIvf1mJT(n#0Q8u^5#9F7ADQY&>H`USRKeII8+Zs)d2P>m3jbB$g{E^8NTANNQ zaBx+Ht6M~T43W)F&?d^dO;I1`trpn4KiU2SRlx4F`r$+C0br=_BlAO>(>2n{t7dQY zvK05I?U&KOjtAKByB4S-qPI|(eEQGNs&OGo)Jt=jx3RLMl#rv1kfV)|qm7WmRgm*2 z|M@)1ze%)^-38=mBjjk~kE4x{!GN6kg`7J4n}b^aHW0?{cTI>m3UQ?vjc zK0r-wW`TwN7;5pUyx_RthcI!j-E}e;UbHmw!Kpl>=xEtA`kQ$e#r~)VK^kg>3mB6)ZyCWk{@t2w zv~Zi5gqz?U@=q0RO;48{amr%oSZkk(122jdQt(LF`9o_eR=tszTu%)q44y|)png=P zKV5}K6c)cbUlS3Nc$CC_hxJIrf4e#BAq*t^_#bziz`7$Y*c!J2Spz`U0Kfej;BT#Q zvgrdDuyM%bu2yTE)wxnp5AI6DGC$&@^e+UL*zKOf1`<&h{j$bk5uvD_*+Px|u*Tgp zfSvF^4e+-o{=w*S|Dyr^^N*Xp8P6NRmm|S9eV3n22CRPc0q4TxnyOY=l=$#KA^@k~0% zMG5IKc~VG!=#C$Y5KIXTx`Kq2%Hf%Sx(hZ9j^xr&N?AK$8$?*=t?N25}6#*}kbCj9}x9;RB-rcw~#-^KV=c zVQ}zSFr->|u3@WkNR#^(fN%OTs(K{KVirG`31=5O{nfMt2?Rl2#E=*9j~DUbKYS2> zy>X67ys=4!jt1^D8@a9w?)hOACYIrRpM?65nE>uI0|$cO1+aAhKQ8J1vvh25g~LJPIt z<-ofpL%ddDG~jU_Fpst2M+6hiQMh$xpy*E;DHhIS*&T$4N4uh{o6*2(wSZ~%se*_| z33}OF2#Ckp=>z+yV-g9k2+HlO_ODq1X)qxwgAgJJA^PnJ(aPWb0B(gH3=j}V!9ReW z>q@CVegHM^;X~-2|133pj)@}h$GL(3IVXpBoFQM1@F(G7c3fY@BwHF%O7k(Ur z>`ftiQ^?*FvNwh7O~qwIp9zXRrWaGt5to#ekd>DrgA4{_Fd%~g8H}2a%+mU-#ORzz zx15~RoV>zZEXZI$1_Lq}kipmmJ)F%voZmd$Zav&yKHTj;-2bl!Lmq4>fP)wcAbQ(x zueUJ*9#ZkI71xBZ;1QdvMA#K0eYLLm#fx5V2oa$?BAszPSq+XR#L~p$#3FQ|wYYM>ON^llKy49*4W~VT?)^CJ zvr%Ef)Cs<3S37Po@QUXzRvl&VsOLg9Q;<>r?MHq6Z$=$YcfK=gAMPeORJjQ);m8tfie|oL_5T zT>_xYKo=k2MhA1m(-yxTZY;|J#N(1rzD%)NLW>tgv4^1)*{s37<67vxuF)jo;W?bo zo~zlZvXk%G^;j%LB(gKY(Edz#82nkt$S#Du*7?)t2f_6DiO;;BeEw*t1sn9#z*C|7|MpYA_cv3IdVAgP|FfYMbnxhphT6MtU)FBwoeRtizO?~N1z=Ai?5`BnBL%DD*?L_ln#~h1REg>{MAP*3-Tpd4Q))7C5 zo{fSDf|g`2MuZkd!=}f5RxAp`m%qe!@{Hh+3d=ss7&^hnai@aTs>iOH%6e~FZ#`{Z zc#+Ghyhgr<{Nna%4!y(IHsn3IpKV1aoCSuxhvi>td9=aPp9Y|U*zF;9``_Gd|Lt$4 zA5QSYNpvu{pW5y2&*?WVGWcoF<-F(s+tJV&jlk3YryZ^KziY1iIsNixT>%}W77bw} z^RuPsyu^KBEZ@ha5@CsEEV$oOAg80)`1HiQn;l5uXXR9b+VK0TCPjZ{w-Ere*6CJX z&s#V24<%}fDHY7A>QpS|Vz@A=u;`NbR0|}HPST$1(Hbeu4@;CbJT(eN z6he);lV^80osppxHXf?Yw|YIS+`A&bMsOiV#NKN|Bfs!Q^--0?UA$dqnPn04gG30$ zcjp{N4xIMpCB!*4G$SOFgHAB!kucm6dR{*>9U zJD?{VoD5wWz=%!?!+ipdhC3Jyu#x{rhH}0S<=0VZNSAp*ozBLMG`BSPm@+M$;w=)1 zgPHz}QQgObC>`-rRe6QecLDJT92;bhW$w=)*L zBEA&#O-M>mn?Ma@Nqy>S3z3jic3W2JSS%xdH7G)66ki1FvJ?cdfGUiQfKCVwLBwW= z*!f&VuaV)edg31!LQhCh$0vBy9-e^8nh;oF~lBp z*cwcrS9G^xP{|*R;IJRdd%{UAlg{pPW_)vAFqtX#3ffZ(b8jN&NhC zET&&&f{1Q%c6MtWu=Hr8MSR(QoZ&EN{QjACKD{twNfN=Vo@-EhV`UoNN%z8z`|~Aw zSs;D_m&4I2(;Ze%cSqaxYQ0ij1>#HliEg#S_DByNyX{(;cg|6sTsy~g${>W`%)6Ga z`{k8c-`X>HUAGRpiM(w|+YT=-2Tg5{1>4=fXW~d@c&$Xbta>&O@GUu?_acz_jB=Z; z`om5ok$a+`xvqKRzz<~j-;m(11(K)1FND$;6m5iIK4SL)zyiNeLa`zNj3Qws_&TDv z_suo|upgQ>V|d$Ba$}yLX&J>rQx|^;7sMUhjOV#^rA}1d=i7>T#_6_|B%>Zm6(?>I z`X!akz4&#CiQphjf>F+s{1nN!h1c)&S5 zF&#I3NwPftuv<`i+-sgRdU}2+SuPuy|G{oj|&> zlP(nA!;@|dDdy82Yz?c^UVPKC(>`LC!_$89VCJ&{>SU|4LHfe7vmxfj!?R)be&+KL zu1{9yqkKDM=VSce4$sGh5m+uJ#PO^zCZ*}hFQ(*pk1nQ_q*yLzR5h$GXEjaBFXwbz zjxImx2eVww8z);|EtnOSUoBcS9$kI5>u344^yZWGw`JFz@^5P^9^a0>t@Y$>#e}(@w?rX;Mv}t4$xKJosIB*y*rDwy@hl;T2SL2w9`U~a*UR#<>8=<(zED3k!9U#j|ESOX(G~};i~Q4i z#KN!-j^F+8=VCcDh13>J=Kkffd>V>ueF@l~OJjizjL)8_)bIWkzpG`cEqOKEQ2sdx zYvnKXxxdb4dgB7Y^||ZaA^$);aD8s3+=rU2;grXgDX$!Rq+Oj}H$3c=|OTSX{F;Um$JKnjZ?!?wUV_B>j2-wz}DRAihb_dJwVm z?s_nJ5dB67b&}b}FT`VR*xd+c@1x(0;F>erjO5!c+Kl4A+TDy6hG+N^BaUnSC03fY z_)DA|&)%1KB}s;@1XXqOtwc?e;;ked=e@0D{UC9>?&_(Oc~1-jq-8E6nlo4JjURg z-F4h6Jg+yB=U$80r!OjMFN}V=;Ob~m0xI$(KR~FO8(Jx=SuA04Z<=a8sQfaDbXc{4 z`24W?GeP)a%^ZVe4C1};@L^rwrMnWKoF8)lamJY`y&gUgqk|60?%?P{`{+b;GeRT= zfSw|o$<|h!{(=?teES{IBOz)o*7jR?xpye{OTH5=_r#PZUDYHiwn7{jUykXpeL7Ei z8{qU6xJoBZy1Vg%4+#-*HGTXMDF2U$N6rRDJg1qVH=7r;=r^0=qC@&em!c1MwMtxi z==b9oop}@^aBZ%W^5oT42BYAgPNR_2QZ{#|sm_yIxTIH1)k~T&X zbyeu1I_eGn7~$o2?Rk`73`=Y(KRq!Wp#t1`uNZmxKRZ#uC?^Kqd^tebKu`{XauAgJ z1LfdS^VWT;-ggIMP|2oCSA&zMf1(`DduapUuafwmDEHjgP27R~w8mrz{YUb2aQbyx zERp*k88iZUe!m)a`l&)Ca7ny7W^`I0Mv=wxQ2oJNmavX1$@7LXtwdUdFEP;6eG5NI z;+@R=5nnbsf=l9uXM+-3JisOK{9Hj$vENW-jcGF1g3ewZQE&Huj7s$2V|Iu|oSk{tD0bCg|4u2p<8_ z_>N}#*8*^hH%Wc4{}tt4d+JjvP&>Hhgff)48itd!12={N=<)c<#;gFm9U_XS5*R9oBR9W2b5Ps`9<~peGyG@lss*Jt}g9ZWoyHw76s4 zuylT|n`VT)C)J@G@K%O!Pi^~TSS@ewYRBH$U^0(Ay{)4{%hPzYA`b`Y`Zcu)w}2UY=)41wUhx@6^2e7Hv!;T$?$#EiReRx^N$u1FhJM?o&v<6 zAi|#<1Rx*)@psRlUjRXMd!GEyXOMAUB<;@@67Vw!MY-!&Op)jHj}{W}GYHA)3Ja<3 z$1_O4=8H5}@iZ79kaU_=e>{Ucc}9ooOF#Yd8Duv7>lyT~S44h2gMM8R`TyY=^zG|f zw{7@0&kg?@^iA|+p{(|*R07iZMmFMX_}iO6lG%Q}>qkuh>J#1+>fJXm**-p=Yd}5_ zK0djhCqq%>lZU%+=FHJO-AfP|Kdl#!pzC7zs&jGT;yoPv}>LZ8ATjZ(pqiiMMk z^%)h9G&LD94KoKV_!E3V$HYd*^q7vBjgFa}o|J%|otJ@;je(VeA-;~$I*Bo^hRG?4 zg&3WM>nTfIH7hAPD;*yzJqs&6J1ZkSD+@F0V;;7+Dt1~bPP+ikm~yUXBHRMf+~TS{ z&-Hn!`1r^q`RJ(lbYh;UT0f(t6@(P*K??T%PX&7*z_r%l)sJ707AwiX;~&4EKuYGcAHN{PLNqI+ zhO&h+RQHql&ZM%DTD#4s%Qb=HI81V1_LmQ8MdCe$`7Qv^0hR0BT!WY+zD?Ce3CPB1 zu5uqZ`%97@TNX6=pKgz5<49%_G}rBUA~$8~qNep4&s7->XTD1EH(hMHWE>m)Z1rWO zW4HiKq4hv&syANBsl3%qK5q)5#sh61b2D@4Q(l-QeKAAP6Zi;)szoV zTCukO_e#v3N`&m~+!s?h!5g&%6+(^ICaSt_YDwCXy|0rMJrPY*45QpsQ%&FlP11zQ z2Tc@h+TC8HJ6LG!WVmh@?__#h?d@dwz%%Y<2jE)lz7L@dqt6I4>@my56+kMMiYKHo z&wunPY)>rBW>KjylNHvY2r*K2U!)*YTdAZ(1#S%ibXdLk8KJI%j>)!eE^Vd!!&Zr9 zDdWv8QxzTJGv;b)0%WEdT83e(+CIUTDs@w|$fcFDxGh$H**!d*x|e*&%ONUh&a7!E zZb|8SS=<^wgT6?47W=k;6o(V&|g*TJfV#`Cf`kv-5wG zdqmL`LUl1AK_>8MQj&?}(X@h4#+w;*5&3#Cb-hugIc=sMr%yo7wp~R-vLTy=piBX$ zTnpY&o&`()L#)NXhUKqI_C_d;pwAB9#ChL43q|m*x}$qFuDV5AH?4(Wp*4tj3z53% zOsYP4q65WGwM8rZq7M!)k#{h1GuSgW`q7@<8Qrr=2cn%w$>q$V#`3A-0FY&xw2F#RV(Nb=iv8C_i?73MwrRv5`G%!<7rkVrvfMm1OC`>TmqQS0Wg4Fs2K>Z zLa6<3Pwfv7mndR_JO1i={&(#MjH@HNhCdrf{@ieoGBf13I1u=sHyl5x{XaGwAK$*$i(V%G>xM%Oeqj9J8wdp0ztZ!8 z9plmW1(rDL`=NL>==)=cUg-y5r*RvG(mOaAhB3?5 z8-}w}UK&Plp>Z2U@*Tf1isE0a6OIm(hUy4T72+I2=L#w5b*qh$ioB91u;@HOFM=5}j8NUuU{=4w`03 zgAnOs{m_t%-v^zAnC0-X4$((O2_hNg#nnig=hN$kF@&bsAQ={BIsh$-@S|iI1NAZ& zjY~>s2Q5lL675KqF*Re0M&%9LA(j=~$3vD8?V!b+;;t(xtLht)@I%ib)|Yy3-Pu3btupE+0O2K5l>as`X4)%C>EGIsEIJ z+hd|vA3=f$b{(UrawiU*q$7G=NSpn3-B$uA9G;kp%zC}J-NE*Kht?w;?xfyUx&xH) zrgnp%_vvecjOTq0!_ZxFYr~wYl8&SB8&+jwcgrK^7J?7y+7qI|l5Zx_DDTUrE=We- z7%K>kYR;%INjlBq8jdatLE9fq%xZ%T_w4Mj`cSA)7dZ-I`oYVyf|YgpzHr#E7~#B@ zk7pV{pI;_Uiy}B`UHX~34Rt_P3UOa4BVMPWpBo&a?`uJ z_Pkk3U7d+!Ejs8z=G*nCz!S&>tnlr{tiqzP73Y#h9=Kl`p>y&v`?wwPWMsCm4xO}X z2;IzMAs(Sh=uU3;^TwkkJ}ikT3MkxQWuH$xruLMW{4y3Ud?bz-#?yFWC@kRiEqMGq zS}OVlZ*Lv6?2Z%s8ZruxkC@MuG6Eqx0MWwe-8QTtDyu*j-UOhJ6Mi1WmsyPH^u@r6 zQXP`sVbqJu{WS!!i*j%N?zdW)213Fq@!oY^mTKT)${~6Gq5T{0Mq{N*(S-OlZ6oC3a#_Je5Lhn1c8Q4i;4}3u{5R?iWLxEPP20 zlj;a{qFmg`A`0#r4og#yP5lmfsrHHLDEALkq&u8~;hOf*9(zU-AfR*`Fpe^SnVM|5 zXrPtYfil#JT8ES&R$A79B_Se&hAlNoPSiL)oj;t840=e(Pa@j<6afh;RJN|jA<>oD zfJ^@-Nn`3Pa;rIC+p2h&(`JIcq^zIGXpmSJ{zcSB<}i-v!9HWm*Qsr-Igf)4-@-(S>Exo;0=qUrc#`z}yeNf{ks#t6^sZd*A|37xy+apot`qhL1q zP-3?;EIQC#p*%FBQYuDWHv1EL(N{SWfi-#W(PkCJeIE~nvQFi+71K-7eNbL8tIM^x zp_iVdoQh4$&!fFZl6|q5m{{|M3Bz+w(vWdFVxq19?*oGZQ_ggTLSCU@Bv8r!Hs(F1 zny(aGq;h0(P~~uaGuM{A~81;dAb;tx)%^Ct$GMRS z-=v&;&FRUT*G*S45Lz;HmL=#JA`QMVqo~cJm98&lp3>*J8Ch$ z;Yn>%q_v4oXhz1>GS@M{tsu7wb z+h`T2HJB$jH5+H)Y846bSVSd0H7ab-ETL<#2&r#2s8rW1L+7#dn|-R^c%@PCt=`h( zvROBF+p_BX>0t}*yNdoO%bL|@s|KED<>P_}m0R3LE^6;yeNxb97_C3DcOueRJyvh3 z=RUSfe5bt=rQV!TfBd?hNb4)BdTSu}SH0PH8sEm$+O6xqYF!ekPr_Svv^}+}z||>5 z=(p%9Z?-GpX)WU>9qNW7vVVWqqK2oT)~8r+uj0h7O7=~4fRdNnK`yaXg)Uijh^*d0 zvYua=jaPL9f!k4dwpEcA{l%Yj`5M4{2Byn