├── .nvmrc ├── scripts ├── .gitignore ├── wait-for-npm-version-to-be-published.sh └── release.sh ├── packages ├── cli-common │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ └── FailureExit.ts │ ├── tsconfig.build.json │ ├── nodemon.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── core │ ├── docs │ │ ├── .gitignore │ │ └── adr │ │ │ ├── 20201002-use-explicit-architecture-and-ddd-for-the-core-api.md │ │ │ ├── 20201003-markdown-parsing-is-part-of-the-domain.md │ │ │ └── 20201027-adr-link-resolver-in-the-domain.md │ ├── src │ │ ├── application │ │ │ ├── Query.ts │ │ │ ├── Command.ts │ │ │ ├── index.ts │ │ │ ├── QueryHandler.ts │ │ │ └── CommandHandler.ts │ │ ├── polyfills.ts │ │ ├── infrastructure │ │ │ ├── api │ │ │ │ ├── types │ │ │ │ │ ├── index.ts │ │ │ │ │ └── AdrDto.ts │ │ │ │ ├── transformers │ │ │ │ │ ├── index.ts │ │ │ │ │ └── adr-transformers.ts │ │ │ │ └── index.ts │ │ │ ├── di │ │ │ │ └── index.ts │ │ │ ├── file-watcher │ │ │ │ └── index.ts │ │ │ ├── buses │ │ │ │ ├── index.ts │ │ │ │ ├── QueryBus.ts │ │ │ │ └── CommandBus.ts │ │ │ └── config │ │ │ │ ├── index.ts │ │ │ │ ├── Log4brainsConfigNotFoundError.ts │ │ │ │ └── schema.ts │ │ ├── lib │ │ │ ├── cheerio-markdown │ │ │ │ ├── index.ts │ │ │ │ ├── cheerioToMarkdown.ts │ │ │ │ ├── CheerioMarkdownElement.ts │ │ │ │ └── markdown-it-source-map-plugin.ts │ │ │ └── paths.ts │ │ ├── adr │ │ │ ├── application │ │ │ │ ├── repositories │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── AdrTemplateRepository.ts │ │ │ │ │ └── AdrRepository.ts │ │ │ │ ├── commands │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── CreateAdrFromTemplateCommand.ts │ │ │ │ │ └── SupersedeAdrCommand.ts │ │ │ │ ├── command-handlers │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── SupersedeAdrCommandHandler.ts │ │ │ │ │ └── CreateAdrFromTemplateCommandHandler.ts │ │ │ │ ├── queries │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── GetAdrBySlugQuery.ts │ │ │ │ │ ├── SearchAdrsQuery.ts │ │ │ │ │ └── GenerateAdrSlugFromTitleCommand.ts │ │ │ │ ├── query-handlers │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── GenerateAdrSlugFromTitleCommandHandler.ts │ │ │ │ │ ├── SearchAdrsQueryHandler.ts │ │ │ │ │ ├── GetAdrBySlugQueryHandler.ts │ │ │ │ │ └── SearchAdrsQueryHandler.test.ts │ │ │ │ └── index.ts │ │ │ ├── infrastructure │ │ │ │ ├── repositories │ │ │ │ │ └── index.ts │ │ │ │ └── MarkdownAdrLinkResolver.ts │ │ │ └── domain │ │ │ │ ├── MarkdownAdrLinkResolver.ts │ │ │ │ ├── PackageRef.ts │ │ │ │ ├── index.ts │ │ │ │ ├── Author.ts │ │ │ │ ├── Package.ts │ │ │ │ ├── AdrStatus.test.ts │ │ │ │ ├── AdrRelation.ts │ │ │ │ ├── MarkdownAdrLink.ts │ │ │ │ ├── AdrFile.test.ts │ │ │ │ ├── MarkdownAdrLink.test.ts │ │ │ │ ├── AdrStatus.ts │ │ │ │ ├── AdrTemplate.ts │ │ │ │ ├── AdrFile.ts │ │ │ │ ├── AdrRelation.test.ts │ │ │ │ ├── AdrSlug.ts │ │ │ │ ├── AdrTemplate.test.ts │ │ │ │ └── AdrSlug.test.ts │ │ ├── domain │ │ │ ├── AggregateRoot.ts │ │ │ ├── index.ts │ │ │ ├── Entity.ts │ │ │ ├── Log4brainsError.ts │ │ │ ├── ValueObjectArray.ts │ │ │ ├── ValueObject.ts │ │ │ ├── ValueObject.test.ts │ │ │ └── ValueObjectMap.ts │ │ ├── index.ts │ │ ├── decs.d.ts │ │ └── utils.ts │ ├── integration-tests │ │ ├── rw-project │ │ │ ├── packages │ │ │ │ ├── package1 │ │ │ │ │ └── adr │ │ │ │ │ │ ├── .gitignore │ │ │ │ │ │ └── template.md │ │ │ │ └── package2 │ │ │ │ │ └── adr │ │ │ │ │ └── .gitignore │ │ │ ├── .gitignore │ │ │ ├── .log4brains.yml │ │ │ └── docs │ │ │ │ └── adr │ │ │ │ └── template.md │ │ └── ro-project │ │ │ ├── docs │ │ │ └── adr │ │ │ │ ├── template.md │ │ │ │ ├── 20201028-adr-with-no-metadata-no-title.md │ │ │ │ ├── 20201028-adr-with-no-metadata.md │ │ │ │ ├── 20200102-adr-only-with-date.md │ │ │ │ ├── 20201030-draft-adr.md │ │ │ │ ├── 20201029-proposed-adr.md │ │ │ │ ├── 20201029-rejected-adr.md │ │ │ │ ├── adr_with_a_WeIrd-filename.md │ │ │ │ ├── 20200102-adr-without-status.md │ │ │ │ ├── 20200102-adr-without-title.md │ │ │ │ ├── 20201028-superseded-adr.md │ │ │ │ ├── 20201028-adr-without-date.md │ │ │ │ ├── 20200101-first-adr.md │ │ │ │ ├── 20201029-superseder.md │ │ │ │ ├── 20200102-adr-with-intro.md │ │ │ │ └── 20201028-links.md │ │ │ ├── packages │ │ │ ├── package1 │ │ │ │ └── adr │ │ │ │ │ ├── template.md │ │ │ │ │ ├── 20201028-adr-without-date.md │ │ │ │ │ ├── 20200101-first-adr.md │ │ │ │ │ ├── 20201028-links-to-global.md │ │ │ │ │ └── 20201028-links-in-package.md │ │ │ └── package2 │ │ │ │ └── adr │ │ │ │ └── 20201028-links-to-another-package.md │ │ │ └── .log4brains.yml │ ├── tsconfig.build.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── jest.config.js │ ├── README.md │ └── package.json ├── global-cli │ ├── .gitignore │ ├── tsconfig.build.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── src │ │ ├── log4brains │ │ ├── cli.ts │ │ └── index.ts │ └── package.json ├── web │ ├── index.ts │ ├── cli │ │ ├── index.ts │ │ ├── commands │ │ │ └── index.ts │ │ ├── utils.ts │ │ └── cli.ts │ ├── nextjs │ │ ├── src │ │ │ ├── lib │ │ │ │ ├── core-api │ │ │ │ │ ├── noop │ │ │ │ │ │ ├── noop-adrs │ │ │ │ │ │ │ └── .gitignore │ │ │ │ │ │ └── .log4brains.yml │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── getIndexPageMarkdown.ts │ │ │ │ │ └── instance.ts │ │ │ │ ├── search │ │ │ │ │ ├── index.ts │ │ │ │ │ └── instance.ts │ │ │ │ ├── toc-utils │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── TocContainer.ts │ │ │ │ │ ├── Toc.ts │ │ │ │ │ ├── TocSection.ts │ │ │ │ │ └── TocBuilder.ts │ │ │ │ ├── adr-utils.ts │ │ │ │ ├── debug.ts │ │ │ │ ├── slugify.ts │ │ │ │ └── next.ts │ │ │ ├── lib-shared │ │ │ │ ├── search │ │ │ │ │ └── index.ts │ │ │ │ └── types.ts │ │ │ ├── components │ │ │ │ ├── Markdown │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── AdrLink │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── AdrLink.tsx │ │ │ │ │ └── hljs.css │ │ │ │ ├── SearchBox │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── components │ │ │ │ │ │ └── SearchBar │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── SearchBox.stories.tsx │ │ │ │ ├── MarkdownToc │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── MarkdownToc.stories.tsx │ │ │ │ │ └── MarkdownToc.test.tsx │ │ │ │ ├── AdrStatusChip │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── AdrStatusChip.stories.tsx │ │ │ │ │ └── AdrStatusChip.tsx │ │ │ │ ├── MarkdownHeading │ │ │ │ │ ├── index.ts │ │ │ │ │ └── MarkdownHeading.tsx │ │ │ │ ├── TwoColContent │ │ │ │ │ ├── index.ts │ │ │ │ │ └── TwoColContent.tsx │ │ │ │ └── index.ts │ │ │ ├── contexts │ │ │ │ ├── AdrNavContext │ │ │ │ │ ├── index.ts │ │ │ │ │ └── AdrNavContext.ts │ │ │ │ ├── Log4brainsModeContext │ │ │ │ │ ├── index.ts │ │ │ │ │ └── Log4brainsModeContext.ts │ │ │ │ └── index.ts │ │ │ ├── scenes │ │ │ │ ├── AdrScene │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── AdrHeader │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── AdrScene.stories.tsx │ │ │ │ ├── IndexScene │ │ │ │ │ ├── index.ts │ │ │ │ │ └── IndexScene.tsx │ │ │ │ └── index.ts │ │ │ ├── mui │ │ │ │ ├── index.ts │ │ │ │ └── MuiDecorator.tsx │ │ │ ├── layouts │ │ │ │ ├── AdrBrowserLayout │ │ │ │ │ ├── components │ │ │ │ │ │ ├── AdrMenu │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── RoutingProgress │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── RoutingProgress.tsx │ │ │ │ │ │ ├── ConnectedSearchBox │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── ConnectedSearchBox.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── AdrBrowserLayout.stories.tsx │ │ │ │ └── index.ts │ │ │ └── pages │ │ │ │ ├── api │ │ │ │ ├── adr.ts │ │ │ │ ├── search-index.ts │ │ │ │ └── adr │ │ │ │ │ └── [...slugAndMore].ts │ │ │ │ ├── index.tsx │ │ │ │ └── adr │ │ │ │ └── [...slug].tsx │ │ ├── .storybook │ │ │ ├── mocks │ │ │ │ └── index.ts │ │ │ ├── preview-head.html │ │ │ ├── preview.js │ │ │ └── main.js │ │ ├── .npmignore │ │ ├── next-env.d.ts │ │ ├── public │ │ │ ├── favicon.ico │ │ │ └── l4b-static │ │ │ │ ├── Log4brains-og.png │ │ │ │ ├── adr-workflow.png │ │ │ │ ├── Log4brains-logo.png │ │ │ │ └── Log4brains-logo-dark.png │ │ ├── tsconfig.dev.json │ │ ├── jest.config.js │ │ ├── .eslintrc.js │ │ ├── .babelrc │ │ ├── tsconfig.json │ │ └── next.config.js │ ├── tsconfig.build.json │ ├── .eslintrc.js │ ├── docs │ │ └── adr │ │ │ ├── 20200927-use-react-hooks.md │ │ │ ├── 20200926-react-file-structure-organized-by-feature.md │ │ │ ├── 20201007-next-js-persistent-layout-pattern.md │ │ │ └── 20200927-avoid-react-fc-type.md │ ├── README.md │ ├── tsconfig.json │ └── .gitignore ├── cli │ ├── src │ │ ├── index.ts │ │ ├── commands │ │ │ ├── index.ts │ │ │ └── ListCommand.ts │ │ └── utils.ts │ ├── tsconfig.build.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── README.md │ └── package.json └── init │ ├── src │ ├── commands │ │ └── index.ts │ ├── index.ts │ ├── utils.ts │ └── cli.ts │ ├── tsconfig.build.json │ ├── .eslintrc.js │ ├── integration-tests │ └── fake-entrypoint.ts │ ├── tsconfig.json │ ├── jest.config.js │ ├── README.md │ ├── assets │ ├── use-log4brains-to-manage-the-adrs.md │ ├── README.md │ ├── use-markdown-architectural-decision-records.md │ └── index.md │ └── package.json ├── docker ├── .gitignore ├── Dockerfile ├── README.md └── Makedockfile.conf ├── docs ├── demo.gif ├── Log4brains-logo-full.png └── adr │ ├── 20200927-avoid-default-exports.md │ ├── README.md │ ├── 20200926-use-the-adr-number-as-its-unique-id.md │ ├── 20201026-the-core-api-is-responsible-for-enhancing-the-adr-markdown-body-with-mdx.md │ ├── 20201103-use-lunr-for-search.md │ ├── 20201016-use-the-adr-slug-as-its-unique-id.md │ ├── 20200925-use-prettier-eslint-airbnb-for-the-code-style.md │ ├── 20241217-switch-back-to-github-flow.md │ ├── 20200925-multi-packages-architecture-in-a-monorepo-with-yarn-and-lerna.md │ ├── 20200924-use-markdown-architectural-decision-records.md │ ├── 20210113-distribute-log4brains-as-a-global-npm-package.md │ └── index.md ├── .prettierignore ├── .github ├── supported_nodejs_versions.json ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature.md │ └── bug.md ├── workflows │ ├── reusable-quality-checks.yml │ ├── reusable-get-log4brains-version.yml │ ├── reusable-wait-for-npm-version-to-be-published.yml │ ├── reusable-load-nodejs-supported-versions.yml │ ├── pull-request-checks.yml │ ├── reusable-e2e-tests.yml │ └── scheduled-weekly-e2e-stable.yml └── auto-issue-templates │ └── new-node-lts.md ├── commitlint.config.js ├── jest.config.base.js ├── .editorconfig ├── e2e-tests ├── package.json └── e2e-launcher.js ├── lerna.json ├── .log4brains.yml ├── tsconfig.json ├── .vscode └── settings.json └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | *.local.sh 2 | -------------------------------------------------------------------------------- /packages/cli-common/.gitignore: -------------------------------------------------------------------------------- 1 | dist-dev 2 | -------------------------------------------------------------------------------- /packages/core/docs/.gitignore: -------------------------------------------------------------------------------- 1 | typedoc 2 | -------------------------------------------------------------------------------- /packages/global-cli/.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /packages/web/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cli"; 2 | -------------------------------------------------------------------------------- /packages/web/cli/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cli"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/core-api/noop/noop-adrs/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createCli } from "./cli"; 2 | -------------------------------------------------------------------------------- /packages/core/src/application/Query.ts: -------------------------------------------------------------------------------- 1 | export abstract class Query {} 2 | -------------------------------------------------------------------------------- /packages/core/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import "core-js/features/array/flat"; 2 | -------------------------------------------------------------------------------- /packages/init/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./InitCommand"; 2 | -------------------------------------------------------------------------------- /packages/init/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createInitCli } from "./cli"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/.storybook/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./adrs"; 2 | -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | Makedockfile.dist.conf 3 | Makedockfile.out 4 | 5 | -------------------------------------------------------------------------------- /packages/core/integration-tests/rw-project/packages/package1/adr/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/integration-tests/rw-project/packages/package2/adr/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/src/application/Command.ts: -------------------------------------------------------------------------------- 1 | export abstract class Command {} 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/search/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./instance"; 2 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomvaill/log4brains/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /packages/core/integration-tests/rw-project/.gitignore: -------------------------------------------------------------------------------- 1 | *.md 2 | !template.md 3 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/api/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrDto"; 2 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/di/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./buildContainer"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib-shared/search/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Search"; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | dist/ 4 | CHANGELOG.md 5 | /lerna.json 6 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/Markdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Markdown"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/SearchBox/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SearchBox"; 2 | -------------------------------------------------------------------------------- /.github/supported_nodejs_versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "versions": ["18.x", "20.x", "22.x"] 3 | } 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/file-watcher/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./FileWatcher"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/MarkdownToc/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MarkdownToc"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/contexts/AdrNavContext/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrNavContext"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/scenes/AdrScene/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrHeader"; 2 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/api/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./adr-transformers"; 2 | -------------------------------------------------------------------------------- /packages/web/cli/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./preview"; 2 | export * from "./build"; 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/AdrStatusChip/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrStatusChip"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/Markdown/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrLink"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/MarkdownHeading/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MarkdownHeading"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/TwoColContent/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./TwoColContent"; 2 | -------------------------------------------------------------------------------- /packages/cli/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ListCommand"; 2 | export * from "./NewCommand"; 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/.npmignore: -------------------------------------------------------------------------------- 1 | .next-export 2 | .storybook 3 | jest.config.js 4 | tsconfig.dev.json 5 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/Markdown/components/AdrLink/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrLink"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/Markdown/hljs.css: -------------------------------------------------------------------------------- 1 | .hljs { 2 | padding: 14px !important; 3 | } 4 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/mui/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MuiDecorator"; 2 | export * from "./theme"; 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/scenes/AdrScene/components/AdrHeader/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrHeader"; 2 | -------------------------------------------------------------------------------- /jest.config.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node" 4 | }; 5 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./Log4brains"; 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/SearchBox/components/SearchBar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SearchBar"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/contexts/Log4brainsModeContext/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Log4brainsModeContext"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/AdrBrowserLayout/components/AdrMenu/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrMenu"; 2 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/buses/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CommandBus"; 2 | export * from "./QueryBus"; 3 | -------------------------------------------------------------------------------- /docs/Log4brains-logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomvaill/log4brains/HEAD/docs/Log4brains-logo-full.png -------------------------------------------------------------------------------- /packages/web/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrNavContext"; 2 | export * from "./Log4brainsModeContext"; 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/AdrBrowserLayout/components/RoutingProgress/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RoutingProgress"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/core-api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./instance"; 2 | export * from "./getIndexPageMarkdown"; 3 | -------------------------------------------------------------------------------- /packages/core/src/lib/cheerio-markdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CheerioMarkdown"; 2 | export * from "./cheerioToMarkdown"; 3 | -------------------------------------------------------------------------------- /packages/core/src/lib/paths.ts: -------------------------------------------------------------------------------- 1 | export function forceUnixPath(p: string): string { 2 | return p.replace(/\\/g, "/"); 3 | } 4 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-cycle 2 | export * from "./AdrBrowserLayout"; 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/scenes/AdrScene/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-cycle 2 | export * from "./AdrScene"; 3 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/web/nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomvaill/log4brains/HEAD/packages/web/nextjs/public/favicon.ico -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ConnectedSearchBox"; 2 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/scenes/IndexScene/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-cycle 2 | export * from "./IndexScene"; 3 | -------------------------------------------------------------------------------- /packages/web/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrRepository"; 2 | export * from "./AdrTemplateRepository"; 3 | -------------------------------------------------------------------------------- /packages/global-cli/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/cli-common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AppConsole"; 2 | export * from "./ConsoleCapturer"; 3 | export * from "./FailureExit"; 4 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/AdrBrowserLayout/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrMenu"; 2 | export * from "./ConnectedSearchBox"; 3 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CreateAdrFromTemplateCommand"; 2 | export * from "./SupersedeAdrCommand"; 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/scenes/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-cycle */ 2 | export * from "./AdrScene"; 3 | export * from "./IndexScene"; 4 | -------------------------------------------------------------------------------- /packages/cli-common/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dev-tests", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/template.md: -------------------------------------------------------------------------------- 1 | # [short title of solved problem and solution] 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "integration-tests", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/init/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "integration-tests", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/web/nextjs/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules"] // include .test.ts and .stories.tsx files 4 | } 5 | -------------------------------------------------------------------------------- /packages/cli-common/src/FailureExit.ts: -------------------------------------------------------------------------------- 1 | export class FailureExit extends Error { 2 | constructor() { 3 | super("The CLI exited with an error"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/packages/package1/adr/template.md: -------------------------------------------------------------------------------- 1 | # [short title of solved problem and solution] 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/command-handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CreateAdrFromTemplateCommandHandler"; 2 | export * from "./SupersedeAdrCommandHandler"; 3 | -------------------------------------------------------------------------------- /packages/web/nextjs/public/l4b-static/Log4brains-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomvaill/log4brains/HEAD/packages/web/nextjs/public/l4b-static/Log4brains-og.png -------------------------------------------------------------------------------- /packages/web/nextjs/public/l4b-static/adr-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomvaill/log4brains/HEAD/packages/web/nextjs/public/l4b-static/adr-workflow.png -------------------------------------------------------------------------------- /packages/core/src/application/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Command"; 2 | export * from "./CommandHandler"; 3 | export * from "./Query"; 4 | export * from "./QueryHandler"; 5 | -------------------------------------------------------------------------------- /packages/web/nextjs/public/l4b-static/Log4brains-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomvaill/log4brains/HEAD/packages/web/nextjs/public/l4b-static/Log4brains-logo.png -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/toc-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Toc"; 2 | export * from "./TocBuilder"; 3 | export * from "./TocContainer"; 4 | export * from "./TocSection"; 5 | -------------------------------------------------------------------------------- /packages/core/src/adr/infrastructure/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrRepository"; 2 | export * from "./AdrTemplateRepository"; 3 | export * from "./PackageRepository"; 4 | -------------------------------------------------------------------------------- /packages/web/nextjs/public/l4b-static/Log4brains-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomvaill/log4brains/HEAD/packages/web/nextjs/public/l4b-static/Log4brains-logo-dark.png -------------------------------------------------------------------------------- /packages/core/src/adr/application/queries/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./GenerateAdrSlugFromTitleCommand"; 2 | export * from "./GetAdrBySlugQuery"; 3 | export * from "./SearchAdrsQuery"; 4 | -------------------------------------------------------------------------------- /packages/core/src/domain/AggregateRoot.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "./Entity"; 2 | 3 | export abstract class AggregateRoot< 4 | T extends Record 5 | > extends Entity {} 6 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/AdrBrowserLayout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrBrowserLayout"; 2 | // eslint-disable-next-line import/no-cycle 3 | export * from "./ConnectedAdrBrowserLayout"; 4 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata-no-title.md: -------------------------------------------------------------------------------- 1 | ## Context and Problem Statement 2 | 3 | Lorem ipsum. 4 | 5 | ## Decision Outcome 6 | 7 | Lorem ipsum. 8 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/adr-utils.ts: -------------------------------------------------------------------------------- 1 | import { Adr, AdrLight } from "../lib-shared/types"; 2 | 3 | export function buildAdrUrl(adr: AdrLight | Adr): string { 4 | return `/adr/${adr.slug}`; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/query-handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./GenerateAdrSlugFromTitleCommandHandler"; 2 | export * from "./GetAdrBySlugQueryHandler"; 3 | export * from "./SearchAdrsQueryHandler"; 4 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./builders"; 2 | export { Log4brainsConfig, GitProvider, GitRepositoryConfig } from "./schema"; 3 | export * from "./Log4brainsConfigNotFoundError"; 4 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/toc-utils/TocContainer.ts: -------------------------------------------------------------------------------- 1 | export interface TocContainer { 2 | parent: TocContainer | null; 3 | getLevel(): number; 4 | createChild(title: string, id: string): TocContainer; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./command-handlers"; 2 | export * from "./commands"; 3 | export * from "./queries"; 4 | export * from "./query-handlers"; 5 | export * from "./repositories"; 6 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/debug.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | export function debug(message: string): void { 4 | if (process.env.NODE_ENV === "development") { 5 | console.log(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201028-adr-with-no-metadata.md: -------------------------------------------------------------------------------- 1 | # ADR with no metadata 2 | 3 | ## Context and Problem Statement 4 | 5 | Lorem ipsum. 6 | 7 | ## Decision Outcome 8 | 9 | Lorem ipsum. 10 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/repositories/AdrTemplateRepository.ts: -------------------------------------------------------------------------------- 1 | import { AdrTemplate, PackageRef } from "@src/adr/domain"; 2 | 3 | export interface AdrTemplateRepository { 4 | find(packageRef?: PackageRef): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AggregateRoot"; 2 | export * from "./Entity"; 3 | export * from "./Log4brainsError"; 4 | export * from "./ValueObject"; 5 | export * from "./ValueObjectArray"; 6 | export * from "./ValueObjectMap"; 7 | -------------------------------------------------------------------------------- /packages/cli-common/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src", "dev-tests"], 3 | "ext": "ts", 4 | "exec": "clear && microbundle --no-compress --format cjs --target node --output dist-dev --entry ./dev-tests/run.ts && node dist-dev/cli-common.js" 5 | } 6 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdrStatusChip"; 2 | export * from "./Markdown"; 3 | export * from "./MarkdownHeading"; 4 | export * from "./MarkdownToc"; 5 | export * from "./SearchBox"; 6 | export * from "./TwoColContent"; 7 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20200102-adr-only-with-date.md: -------------------------------------------------------------------------------- 1 | # ADR only with date 2 | 3 | - Date: 2020-01-02 4 | 5 | ## Context and Problem Statement 6 | 7 | Lorem ipsum. 8 | 9 | ## Decision Outcome 10 | 11 | Lorem ipsum. 12 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201030-draft-adr.md: -------------------------------------------------------------------------------- 1 | # Draft ADR 2 | 3 | - Status: draft 4 | - Date: 2020-10-30 5 | 6 | ## Context and Problem Statement 7 | 8 | Lorem ipsum. 9 | 10 | ## Decision Outcome 11 | 12 | Lorem ipsum. 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /packages/cli/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | env: { 5 | node: true 6 | }, 7 | parserOptions: { 8 | project: path.join(__dirname, "tsconfig.json") 9 | }, 10 | extends: ["../../.eslintrc"] 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | env: { 5 | node: true 6 | }, 7 | parserOptions: { 8 | project: path.join(__dirname, "tsconfig.json") 9 | }, 10 | extends: ["../../.eslintrc"] 11 | }; 12 | -------------------------------------------------------------------------------- /packages/init/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | env: { 5 | node: true 6 | }, 7 | parserOptions: { 8 | project: path.join(__dirname, "tsconfig.json") 9 | }, 10 | extends: ["../../.eslintrc"] 11 | }; 12 | -------------------------------------------------------------------------------- /packages/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | env: { 5 | node: true 6 | }, 7 | parserOptions: { 8 | project: path.join(__dirname, "tsconfig.json") 9 | }, 10 | extends: ["../../.eslintrc"] 11 | }; 12 | -------------------------------------------------------------------------------- /packages/cli-common/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | env: { 5 | node: true 6 | }, 7 | parserOptions: { 8 | project: path.join(__dirname, "tsconfig.json") 9 | }, 10 | extends: ["../../.eslintrc"] 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201029-proposed-adr.md: -------------------------------------------------------------------------------- 1 | # Proposed ADR 2 | 3 | - Status: proposed 4 | - Date: 2020-10-29 5 | 6 | ## Context and Problem Statement 7 | 8 | Lorem ipsum. 9 | 10 | ## Decision Outcome 11 | 12 | Lorem ipsum. 13 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201029-rejected-adr.md: -------------------------------------------------------------------------------- 1 | # Rejected ADR 2 | 3 | - Status: rejected 4 | - Date: 2020-10-29 5 | 6 | ## Context and Problem Statement 7 | 8 | Lorem ipsum. 9 | 10 | ## Decision Outcome 11 | 12 | Lorem ipsum. 13 | -------------------------------------------------------------------------------- /packages/core/src/domain/Entity.ts: -------------------------------------------------------------------------------- 1 | export abstract class Entity> { 2 | constructor(public readonly props: T) {} 3 | 4 | public equals(e?: Entity): boolean { 5 | return e === this; // One instance allowed per entity 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/global-cli/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | env: { 5 | node: true 6 | }, 7 | parserOptions: { 8 | project: path.join(__dirname, "tsconfig.json") 9 | }, 10 | extends: ["../../.eslintrc"] 11 | }; 12 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/core-api/noop/.log4brains.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This config file is used by instance.ts during Next.js build phase, 3 | # When we want to create a noop instance of Log4brains 4 | project: 5 | name: noop 6 | tz: Etc/UTC 7 | adrFolder: ./noop-adrs 8 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/MarkdownAdrLinkResolver.ts: -------------------------------------------------------------------------------- 1 | import { Adr } from "./Adr"; 2 | import { MarkdownAdrLink } from "./MarkdownAdrLink"; 3 | 4 | export interface MarkdownAdrLinkResolver { 5 | resolve(from: Adr, uri: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/slugify.ts: -------------------------------------------------------------------------------- 1 | import slugifyFn from "slugify"; 2 | 3 | // used to slugify markdown paragraph IDs 4 | export function slugify(string: string): string { 5 | return slugifyFn(string, { 6 | lower: true, 7 | strict: true 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/adr_with_a_WeIrd-filename.md: -------------------------------------------------------------------------------- 1 | # ADR with a weird filename 2 | 3 | - Status: accepted 4 | - Date: 2020-01-02 5 | 6 | ## Context and Problem Statement 7 | 8 | Lorem ipsum. 9 | 10 | ## Decision Outcome 11 | 12 | Lorem ipsum. 13 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/queries/GetAdrBySlugQuery.ts: -------------------------------------------------------------------------------- 1 | import { AdrSlug } from "@src/adr/domain"; 2 | import { Query } from "@src/application"; 3 | 4 | export class GetAdrBySlugQuery extends Query { 5 | constructor(public readonly slug: AdrSlug) { 6 | super(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /e2e-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e-tests", 3 | "description": "Log4brains E2E tests sub-package", 4 | "private": true, 5 | "devDependencies": { 6 | "chai": "^4.2.0", 7 | "chalk": "^4.1.0", 8 | "execa": "^4.1.0", 9 | "rimraf": "^3.0.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/init/integration-tests/fake-entrypoint.ts: -------------------------------------------------------------------------------- 1 | import { AppConsole } from "@log4brains/cli-common"; 2 | import { createInitCli } from "../src/cli"; 3 | 4 | const cli = createInitCli({ 5 | appConsole: new AppConsole({ debug: false, traces: false }) 6 | }); 7 | cli.parse(process.argv); 8 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/contexts/Log4brainsModeContext/Log4brainsModeContext.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export enum Log4brainsMode { 4 | preview = "preview", 5 | static = "static" 6 | } 7 | 8 | export const Log4brainsModeContext = React.createContext(Log4brainsMode.static); 9 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-status.md: -------------------------------------------------------------------------------- 1 | # ADR without status 2 | 3 | - Deciders: John Doe 4 | - Date: 2020-01-02 5 | - Tags: foo 6 | 7 | ## Context and Problem Statement 8 | 9 | Lorem ipsum. 10 | 11 | ## Decision Outcome 12 | 13 | Lorem ipsum. 14 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/config/Log4brainsConfigNotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { Log4brainsError } from "@src/domain"; 2 | 3 | export class Log4brainsConfigNotFoundError extends Log4brainsError { 4 | constructor() { 5 | super("Impossible to find the .log4brains.yml config file"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "command": { 9 | "version": { 10 | "message": "chore(release): publish %s", 11 | "allowBranch": "develop" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./polyfills"; 2 | 3 | export * from "./infrastructure/api"; 4 | export * from "./infrastructure/file-watcher"; 5 | export { Log4brainsError } from "./domain"; 6 | export { 7 | Log4brainsConfig, 8 | Log4brainsConfigNotFoundError 9 | } from "./infrastructure/config"; 10 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20200102-adr-without-title.md: -------------------------------------------------------------------------------- 1 | - Deciders: John Doe, Foo bar 2 | - Date: 2020-01-02 3 | - Tags: foo, bar 4 | 5 | Technical Story: lorem ipsum 6 | 7 | ## Context and Problem Statement 8 | 9 | Lorem ipsum. 10 | 11 | ## Decision Outcome 12 | 13 | Lorem ipsum. 14 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/contexts/AdrNavContext/AdrNavContext.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AdrLight } from "../../lib-shared/types"; 3 | 4 | export type AdrNav = { 5 | previousAdr?: AdrLight; 6 | nextAdr?: AdrLight; 7 | }; 8 | 9 | export const AdrNavContext = React.createContext({}); 10 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201028-superseded-adr.md: -------------------------------------------------------------------------------- 1 | # Superseded ADR 2 | 3 | - Status: superseded by [20201029-superseder](20201029-superseder.md) 4 | - Date: 2020-10-28 5 | 6 | ## Context and Problem Statement 7 | 8 | Lorem ipsum. 9 | 10 | ## Decision Outcome 11 | 12 | Lorem ipsum. 13 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/packages/package2/adr/20201028-links-to-another-package.md: -------------------------------------------------------------------------------- 1 | # Links to another package 2 | 3 | - Date: 2020-10-28 4 | 5 | ## Test 6 | 7 | Test link: [package1/20200101-first-adr](../../package1/adr/20200101-first-adr.md) 8 | [Custom text](../../package1/adr/20200101-first-adr.md) 9 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | ARG LOG4BRAINS_VERSION 4 | 5 | USER node 6 | WORKDIR /workdir 7 | ENV NPM_CONFIG_PREFIX=/home/node/.npm-global 8 | ENV PATH=$PATH:/home/node/.npm-global/bin 9 | 10 | RUN npm install -g log4brains@${LOG4BRAINS_VERSION} 11 | 12 | EXPOSE 4004 13 | ENTRYPOINT [ "log4brains" ] 14 | -------------------------------------------------------------------------------- /packages/web/nextjs/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../../jest.config.base"); 2 | const packageJson = require("./package"); 3 | 4 | module.exports = { 5 | ...base, 6 | name: packageJson.name, 7 | displayName: packageJson.name, 8 | transform: { 9 | "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.log4brains.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project: 3 | name: Log4brains 4 | tz: Europe/Paris 5 | adrFolder: ./docs/adr 6 | 7 | packages: 8 | - name: core 9 | path: ./packages/core 10 | adrFolder: ./packages/core/docs/adr 11 | 12 | - name: web 13 | path: ./packages/web 14 | adrFolder: ./packages/web/docs/adr 15 | -------------------------------------------------------------------------------- /packages/web/docs/adr/20200927-use-react-hooks.md: -------------------------------------------------------------------------------- 1 | # Use React hooks 2 | 3 | - Status: accepted 4 | - Date: 2020-09-27 5 | 6 | ## Decision 7 | 8 | We will use React hooks and avoid class components. 9 | 10 | ## Links 11 | 12 | - [Why We Switched to React Hooks](https://blog.bitsrc.io/why-we-switched-to-react-hooks-48798c42c7f) 13 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/commands/CreateAdrFromTemplateCommand.ts: -------------------------------------------------------------------------------- 1 | import { AdrSlug } from "@src/adr/domain"; 2 | import { Command } from "@src/application"; 3 | 4 | export class CreateAdrFromTemplateCommand extends Command { 5 | constructor(public readonly slug: AdrSlug, public readonly title: string) { 6 | super(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/decs.d.ts: -------------------------------------------------------------------------------- 1 | declare module "markdown-it-source-map"; 2 | 3 | declare module "launch-editor" { 4 | export default function ( 5 | path: string, 6 | specifiedEditor?: string, 7 | onErrorCallback?: ( 8 | filename: string, 9 | message?: string 10 | ) => void | Promise 11 | ): void; 12 | } 13 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Log4brains 2 | 3 | Official Docker image of the Log4brains CLI: 4 | 5 | ## Usage 6 | 7 | ```bash 8 | docker run --rm -ti -v $(pwd):/workdir -p 4004:4004 thomvaill/log4brains help 9 | ``` 10 | 11 | Note: the browser auto-open feature is not available when running from Docker. 12 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201028-adr-without-date.md: -------------------------------------------------------------------------------- 1 | # ADR without date 2 | 3 | - Status: accepted 4 | - Deciders: John Doe, Foo bar 5 | - Tags: foo, bar 6 | 7 | Technical Story: lorem ipsum 8 | 9 | ## Context and Problem Statement 10 | 11 | Lorem ipsum. 12 | 13 | ## Decision Outcome 14 | 15 | Lorem ipsum. 16 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/PackageRef.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from "@src/domain"; 2 | 3 | type Props = { 4 | name: string; 5 | }; 6 | 7 | export class PackageRef extends ValueObject { 8 | constructor(name: string) { 9 | super({ name }); 10 | } 11 | 12 | get name(): string { 13 | return this.props.name; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/application/QueryHandler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Query } from "./Query"; 3 | 4 | export interface QueryHandler { 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | readonly queryClass: Function; 7 | execute(query: Q): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /packages/cli/src/utils.ts: -------------------------------------------------------------------------------- 1 | import execa from "execa"; 2 | 3 | export async function previewAdr(slug: string): Promise { 4 | const subprocess = execa("log4brains", ["preview", slug], { 5 | stdio: "inherit" 6 | }); 7 | subprocess.stdout?.pipe(process.stdout); 8 | subprocess.stderr?.pipe(process.stderr); 9 | await subprocess; 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20200101-first-adr.md: -------------------------------------------------------------------------------- 1 | # First ADR 2 | 3 | - Status: accepted 4 | - Deciders: John Doe, Foo bar 5 | - Date: 2020-01-01 6 | - Tags: foo, bar 7 | 8 | Technical Story: lorem ipsum 9 | 10 | ## Context and Problem Statement 11 | 12 | Lorem ipsum. 13 | 14 | ## Decision Outcome 15 | 16 | Lorem ipsum. 17 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201029-superseder.md: -------------------------------------------------------------------------------- 1 | # Superseder 2 | 3 | - Status: accepted 4 | - Date: 2020-10-29 5 | 6 | ## Context and Problem Statement 7 | 8 | Lorem ipsum. 9 | 10 | ## Decision Outcome 11 | 12 | Lorem ipsum. 13 | 14 | ## Links 15 | 16 | - Supersedes [20201028-superseded-adr](20201028-superseded-adr.md) 17 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/repositories/AdrRepository.ts: -------------------------------------------------------------------------------- 1 | import { Adr, AdrSlug, PackageRef } from "@src/adr/domain"; 2 | 3 | export interface AdrRepository { 4 | find(slug: AdrSlug): Promise; 5 | findAll(): Promise; 6 | generateAvailableSlug(title: string, packageRef?: PackageRef): AdrSlug; 7 | save(adr: Adr): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Give your feedback 📣 3 | url: https://github.com/thomvaill/log4brains/discussions/new?category=Feedback 4 | about: Give your feedback 5 | - name: Ask a question 6 | url: https://github.com/thomvaill/log4brains/discussions 7 | about: Ask questions and discuss with other community members 8 | -------------------------------------------------------------------------------- /packages/core/src/application/CommandHandler.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "./Command"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export interface CommandHandler { 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | readonly commandClass: Function; 7 | execute(command: C): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/domain/Log4brainsError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Log4brains Error base class. 3 | * Any error thrown by the core API extends this class. 4 | */ 5 | export class Log4brainsError extends Error { 6 | constructor(public readonly name: string, public readonly details?: string) { 7 | super(`${name}${details ? ` (${details})` : ""}`); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/packages/package1/adr/20201028-adr-without-date.md: -------------------------------------------------------------------------------- 1 | # ADR without date (in package 1) 2 | 3 | - Status: accepted 4 | - Deciders: John Doe, Foo bar 5 | - Tags: foo, bar 6 | 7 | Technical Story: lorem ipsum 8 | 9 | ## Context and Problem Statement 10 | 11 | Lorem ipsum. 12 | 13 | ## Decision Outcome 14 | 15 | Lorem ipsum. 16 | -------------------------------------------------------------------------------- /packages/core/src/domain/ValueObjectArray.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { ValueObject } from "./ValueObject"; 3 | 4 | export class ValueObjectArray { 5 | static inArray>( 6 | object: VO, 7 | array: VO[] 8 | ): boolean { 9 | return array.some((o) => o.equals(object)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/packages/package1/adr/20200101-first-adr.md: -------------------------------------------------------------------------------- 1 | # First ADR (in package 1) 2 | 3 | - Status: accepted 4 | - Deciders: John Doe, Foo bar 5 | - Date: 2020-01-01 6 | - Tags: foo, bar 7 | 8 | Technical Story: lorem ipsum 9 | 10 | ## Context and Problem Statement 11 | 12 | Lorem ipsum. 13 | 14 | ## Decision Outcome 15 | 16 | Lorem ipsum. 17 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-to-global.md: -------------------------------------------------------------------------------- 1 | # Links to global 2 | 3 | - Date: 2020-10-28 4 | 5 | ## Context and Problem Statement 6 | 7 | Lorem ipsum. Test link: [20200101-first-adr](../../docs/adr/20200101-first-adr.md) 8 | 9 | ## Decision Outcome 10 | 11 | - Relates to [20201028-links](../../docs/adr/20201028-links.md) 12 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/commands/SupersedeAdrCommand.ts: -------------------------------------------------------------------------------- 1 | import { AdrSlug } from "@src/adr/domain"; 2 | import { Command } from "@src/application"; 3 | 4 | export class SupersedeAdrCommand extends Command { 5 | constructor( 6 | public readonly supersededSlug: AdrSlug, 7 | public readonly supersederSlug: AdrSlug 8 | ) { 9 | super(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/queries/SearchAdrsQuery.ts: -------------------------------------------------------------------------------- 1 | import { AdrStatus } from "@src/adr/domain"; 2 | import { Query } from "@src/application"; 3 | 4 | export type SearchAdrsFilters = { 5 | statuses?: AdrStatus[]; 6 | }; 7 | 8 | export class SearchAdrsQuery extends Query { 9 | constructor(public readonly filters: SearchAdrsFilters) { 10 | super(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/web/nextjs/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/queries/GenerateAdrSlugFromTitleCommand.ts: -------------------------------------------------------------------------------- 1 | import { PackageRef } from "@src/adr/domain"; 2 | import { Query } from "@src/application"; 3 | 4 | export class GenerateAdrSlugFromTitleQuery extends Query { 5 | constructor( 6 | public readonly title: string, 7 | public readonly packageRef?: PackageRef 8 | ) { 9 | super(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/.log4brains.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project: 3 | name: log4brains-tests-ro 4 | tz: Europe/Paris 5 | adrFolder: ./docs/adr 6 | 7 | packages: 8 | - name: package1 9 | path: ./packages/package1 10 | adrFolder: ./packages/package1/adr 11 | - name: package2 12 | path: ./packages/package2 13 | adrFolder: ./packages/package2/adr 14 | -------------------------------------------------------------------------------- /packages/core/integration-tests/rw-project/.log4brains.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project: 3 | name: log4brains-tests-rw 4 | tz: Europe/Paris 5 | adrFolder: ./docs/adr 6 | 7 | packages: 8 | - name: package1 9 | path: ./packages/package1 10 | adrFolder: ./packages/package1/adr 11 | - name: package2 12 | path: ./packages/package2 13 | adrFolder: ./packages/package2/adr 14 | -------------------------------------------------------------------------------- /packages/web/docs/adr/20200926-react-file-structure-organized-by-feature.md: -------------------------------------------------------------------------------- 1 | # React file structure organized by feature 2 | 3 | - Status: accepted 4 | - Date: 2020-09-26 5 | 6 | ## Decision 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20200102-adr-with-intro.md: -------------------------------------------------------------------------------- 1 | # ADR with intro 2 | 3 | This is an introduction paragraph. 4 | 5 | - Status: accepted 6 | - Deciders: John Doe, Foo bar 7 | - Date: 2020-01-02 8 | - Tags: foo, bar 9 | 10 | Technical Story: lorem ipsum 11 | 12 | ## Context and Problem Statement 13 | 14 | Lorem ipsum. 15 | 16 | ## Decision Outcome 17 | 18 | Lorem ipsum. 19 | -------------------------------------------------------------------------------- /docs/adr/20200927-avoid-default-exports.md: -------------------------------------------------------------------------------- 1 | # Avoid default exports 2 | 3 | - Status: accepted 4 | - Date: 2020-09-27 5 | 6 | ## Decision 7 | 8 | We will avoid default exports in all our codebase, and use named exports instead. 9 | 10 | ## Links 11 | 12 | - 13 | - 14 | -------------------------------------------------------------------------------- /packages/cli-common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "baseUrl": ".", 8 | "paths": { 9 | "@src/*": ["src/*"] 10 | } 11 | }, 12 | "include": ["src/**/*.ts", "dev-tests/**/*.ts"], 13 | "exclude": ["node_modules"], 14 | "ts-node": { "files": true } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@src/*": ["src/*"] 11 | } 12 | }, 13 | "include": ["src/**/*.ts"], 14 | "exclude": ["node_modules"], 15 | "ts-node": { "files": true } 16 | } 17 | -------------------------------------------------------------------------------- /packages/global-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@src/*": ["src/*"] 11 | } 12 | }, 13 | "include": ["src/**/*.ts"], 14 | "exclude": ["node_modules"], 15 | "ts-node": { "files": true } 16 | } 17 | -------------------------------------------------------------------------------- /packages/init/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "baseUrl": ".", 8 | "paths": { 9 | "@src/*": ["src/*"] 10 | } 11 | }, 12 | "include": ["src/**/*.ts", "integration-tests/**/*.ts"], 13 | "exclude": ["node_modules"], 14 | "ts-node": { "files": true } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["ES2019.Array"], 5 | "declaration": true, 6 | "declarationMap": true, 7 | "outDir": "dist", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@src/*": ["src/*"] 11 | } 12 | }, 13 | "include": ["src/**/*.ts", "integration-tests/**/*.ts"], 14 | "exclude": ["node_modules"], 15 | "ts-node": { "files": true } 16 | } 17 | -------------------------------------------------------------------------------- /packages/web/docs/adr/20201007-next-js-persistent-layout-pattern.md: -------------------------------------------------------------------------------- 1 | # Next.js persistent layout pattern 2 | 3 | - Status: accepted 4 | - Date: 2020-10-07 5 | 6 | ## Context and Problem Statement 7 | 8 | We don't want the menu scroll position to be changed when we navigate from one page to another. 9 | 10 | ## Decision 11 | 12 | We will use the [Next.js persistent layout pattern](https://adamwathan.me/2019/10/17/persistent-layout-patterns-in-nextjs/). 13 | -------------------------------------------------------------------------------- /packages/init/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require("ts-jest/utils"); 2 | const base = require("../../jest.config.base"); 3 | const { compilerOptions } = require("./tsconfig"); 4 | const packageJson = require("./package"); 5 | 6 | module.exports = { 7 | ...base, 8 | name: packageJson.name, 9 | displayName: packageJson.name, 10 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 11 | prefix: "/" 12 | }) 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", // Transpiled later by Next.js or microbundle with Babel 4 | "module": "esnext", // Microbundle/rollup will also generate a commonJS-compatible package 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "preserve" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Adr"; 2 | export * from "./AdrFile"; 3 | export * from "./AdrRelation"; 4 | export * from "./AdrSlug"; 5 | export * from "./AdrStatus"; 6 | export * from "./AdrTemplate"; 7 | export * from "./Author"; 8 | export * from "./FilesystemPath"; 9 | export * from "./MarkdownAdrLink"; 10 | export * from "./MarkdownAdrLinkResolver"; 11 | export * from "./MarkdownBody"; 12 | export * from "./Package"; 13 | export * from "./PackageRef"; 14 | -------------------------------------------------------------------------------- /docker/Makedockfile.conf: -------------------------------------------------------------------------------- 1 | # Makedockfile global config file 2 | # https://github.com/Thomvaill/Makedockfile/ 3 | # 4 | # You can override this file locally by creating Makedockfile.dist.conf 5 | # To get more help about these variables, see Makefile documentation 6 | 7 | MDF_NAMESPACE ?= thomvaill 8 | MDF_REPOSITORY ?= log4brains 9 | MDF_VERSION_TAG ?= $(shell cat ../lerna.json | jq -r .version) 10 | MDF_BUILD_PARAMS ?= --build-arg LOG4BRAINS_VERSION=$(shell cat ../lerna.json | jq -r .version) 11 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # @log4brains/cli 2 | 3 | This package provides the CLI features of the [Log4brains](https://github.com/thomvaill/log4brains) architecture knowledge base. 4 | It is not meant to be used directly. 5 | 6 | Install [the main log4brains package](https://www.npmjs.com/package/log4brains) instead: 7 | 8 | ```bash 9 | npm install -g log4brains 10 | ``` 11 | 12 | ## Documentation 13 | 14 | - [Log4brains README](https://github.com/thomvaill/log4brains/blob/develop/README.md) 15 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/mui/MuiDecorator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ThemeProvider } from "@material-ui/core/styles"; 3 | import { CssBaseline } from "@material-ui/core"; 4 | import { theme } from "./theme"; 5 | 6 | type Props = { 7 | children: React.ReactNode; 8 | }; 9 | 10 | export function MuiDecorator({ children }: Props) { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/init/README.md: -------------------------------------------------------------------------------- 1 | # @log4brains/init 2 | 3 | This package provides the initialization script of the [Log4brains](https://github.com/thomvaill/log4brains) architecture knowledge base. 4 | It is not meant to be used directly. 5 | 6 | Install [the main log4brains package](https://www.npmjs.com/package/log4brains) instead: 7 | 8 | ```bash 9 | npm install -g log4brains 10 | ``` 11 | 12 | ## Documentation 13 | 14 | - [Log4brains README](https://github.com/thomvaill/log4brains/blob/develop/README.md) 15 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 | # @log4brains/web 2 | 3 | This package provides the static site generation features of the [Log4brains](https://github.com/thomvaill/log4brains) architecture knowledge base. 4 | It is not meant to be used directly. 5 | 6 | Install [the main log4brains package](https://www.npmjs.com/package/log4brains) instead: 7 | 8 | ```bash 9 | npm install -g log4brains 10 | ``` 11 | 12 | ## Documentation 13 | 14 | - [Log4brains README](https://github.com/thomvaill/log4brains/blob/develop/README.md) 15 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "rootDir": ".", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@cli/*": ["cli/*"], 11 | "@lib-shared/*": ["nextjs/src/lib-shared/*"] 12 | } 13 | }, 14 | "include": ["index.ts", "cli/**/*.ts", "nextjs/src/lib-shared/**/*.ts"], 15 | "exclude": ["node_modules"], 16 | "ts-node": { "files": true } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require("ts-jest/utils"); 2 | const base = require("../../jest.config.base"); 3 | const { compilerOptions } = require("./tsconfig"); 4 | const packageJson = require("./package"); 5 | 6 | module.exports = { 7 | ...base, 8 | name: packageJson.name, 9 | displayName: packageJson.name, 10 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 11 | prefix: "/" 12 | }), 13 | setupFiles: ["/src/polyfills.ts"] 14 | }; 15 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/packages/package1/adr/20201028-links-in-package.md: -------------------------------------------------------------------------------- 1 | # Links in package 2 | 3 | - Date: 2020-10-28 4 | 5 | ## Context and Problem Statement 6 | 7 | Test link with complete slug: [package1/20200101-first-adr](20200101-first-adr.md) 8 | Test link with partial slug: [20200101-first-adr](20200101-first-adr.md) 9 | [Custom text and relative path](./20200101-first-adr.md) 10 | 11 | ## Decision Outcome 12 | 13 | - Relates to [package1/20201028-links-to-global](20201028-links-to-global.md) 14 | -------------------------------------------------------------------------------- /packages/core/integration-tests/ro-project/docs/adr/20201028-links.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | - Date: 2020-10-28 4 | 5 | ## Tests 6 | 7 | - Classic link: [20200101-first-adr](20200101-first-adr.md) 8 | - Relative link: [20200101-first-adr](./20200101-first-adr.md) 9 | - [Link with a custom text](20200101-first-adr.md) 10 | - [External link](https://www.google.com/) 11 | - Broken link: [20200101-first-adr](20200101-first-adr-BROKEN.md) 12 | - Link to a package: [package1/20200101-first-adr](../../packages/package1/adr/20200101-first-adr.md) 13 | -------------------------------------------------------------------------------- /packages/core/integration-tests/rw-project/packages/package1/adr/template.md: -------------------------------------------------------------------------------- 1 | # [short title of solved problem and solution] 2 | 3 | - Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)] 4 | - Date: [YYYY-MM-DD when the decision was last updated] 5 | - Tags: [space and/or comma separated list of tags] 6 | 7 | ## Context and Problem Statement 8 | 9 | This is a custom template for this package. 10 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/pages/api/adr.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { getLog4brainsInstance } from "../../lib/core-api"; 3 | import { toAdrLight } from "../../lib-shared/types"; 4 | 5 | export default async ( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ): Promise => { 9 | if (req.method !== "GET") { 10 | res.status(404).send("Not Found"); 11 | return; 12 | } 13 | 14 | res 15 | .status(200) 16 | .json((await getLog4brainsInstance().searchAdrs()).map(toAdrLight)); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /packages/cli-common/README.md: -------------------------------------------------------------------------------- 1 | # @log4brains/cli-common 2 | 3 | This package provides common features for all [Log4brains](https://github.com/thomvaill/log4brains) CLI-based packages. 4 | It is not meant to be used directly. 5 | 6 | Install [the main log4brains package](https://www.npmjs.com/package/log4brains) instead: 7 | 8 | ```bash 9 | npm install -g log4brains 10 | ``` 11 | 12 | ## Documentation 13 | 14 | - [Log4brains README](https://github.com/thomvaill/log4brains/blob/develop/README.md) 15 | 16 | ## Development 17 | 18 | ```bash 19 | yarn dev:test 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/Author.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from "@src/domain"; 2 | 3 | type Props = { 4 | name: string; 5 | email?: string; 6 | }; 7 | 8 | export class Author extends ValueObject { 9 | constructor(name: string, email?: string) { 10 | super({ name, email }); 11 | } 12 | 13 | get name(): string { 14 | return this.props.name; 15 | } 16 | 17 | get email(): string | undefined { 18 | return this.props.email; 19 | } 20 | 21 | static createAnonymous(): Author { 22 | return new Author("Anonymous"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/lib/cheerio-markdown/cheerioToMarkdown.ts: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio"; 2 | 3 | export function cheerioToMarkdown( 4 | elt: cheerio.Cheerio, 5 | keepLinks = true 6 | ): string { 7 | const html = elt.html(); 8 | if (!html) { 9 | return ""; 10 | } 11 | const copy = cheerio.load(html); 12 | 13 | if (keepLinks) { 14 | copy("a").each((i, linkElt) => { 15 | copy(linkElt).text( 16 | `[${copy(linkElt).text()}](${copy(linkElt).attr("href")})` 17 | ); 18 | }); 19 | } 20 | 21 | return copy("body").text(); 22 | } 23 | -------------------------------------------------------------------------------- /packages/web/nextjs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | env: { 5 | browser: true, 6 | node: true 7 | }, 8 | parserOptions: { 9 | ecmaFeatures: { 10 | jsx: true 11 | }, 12 | project: path.join(__dirname, "tsconfig.dev.json") 13 | }, 14 | extends: ["../../../.eslintrc"], 15 | overrides: [ 16 | { 17 | files: ["src/pages/**/*.tsx", "src/pages/api/**/*.ts"], // Next.js pages and api routes 18 | rules: { 19 | "import/no-default-export": "off" 20 | } 21 | } 22 | ] 23 | }; 24 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/Package.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "@src/domain"; 2 | import { FilesystemPath } from "./FilesystemPath"; 3 | import { PackageRef } from "./PackageRef"; 4 | 5 | type Props = { 6 | ref: PackageRef; 7 | path: FilesystemPath; 8 | adrFolderPath: FilesystemPath; 9 | }; 10 | 11 | export class Package extends Entity { 12 | get ref(): PackageRef { 13 | return this.props.ref; 14 | } 15 | 16 | get path(): FilesystemPath { 17 | return this.props.path; 18 | } 19 | 20 | get adrFolderPath(): FilesystemPath { 21 | return this.props.adrFolderPath; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/pages/api/search-index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { getLog4brainsInstance } from "../../lib/core-api"; 3 | import { Search } from "../../lib-shared/search"; 4 | 5 | export default async ( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ): Promise => { 9 | if (req.method !== "GET") { 10 | res.status(404).send("Not Found"); 11 | return; 12 | } 13 | 14 | res 15 | .status(200) 16 | .json( 17 | Search.createFromAdrs( 18 | await getLog4brainsInstance().searchAdrs() 19 | ).serializeIndex() 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrStatus.test.ts: -------------------------------------------------------------------------------- 1 | import { AdrStatus } from "./AdrStatus"; 2 | 3 | describe("AdrStatus", () => { 4 | it("create from name", () => { 5 | const status = AdrStatus.createFromName("draft"); 6 | expect(status.name).toEqual("draft"); 7 | }); 8 | 9 | it("throws when unknown name", () => { 10 | expect(() => { 11 | AdrStatus.createFromName("loremipsum"); 12 | }).toThrow(); 13 | }); 14 | 15 | it("works with 'superseded by XXX", () => { 16 | const status = AdrStatus.createFromName("superseded by XXX"); 17 | expect(status.name).toEqual("superseded"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/web/nextjs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | // Source: https://material-ui.com/guides/minimizing-bundle-size/ 5 | [ 6 | "babel-plugin-import", 7 | { 8 | "libraryName": "@material-ui/core", 9 | "libraryDirectory": "", 10 | "camel2DashComponentName": false 11 | }, 12 | "mui-core" 13 | ], 14 | [ 15 | "babel-plugin-import", 16 | { 17 | "libraryName": "@material-ui/icons", 18 | "libraryDirectory": "", 19 | "camel2DashComponentName": false 20 | }, 21 | "mui-icons" 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature Request" 3 | about: "Suggest new features and changes" 4 | labels: feature 5 | --- 6 | 7 | # Feature Request 8 | 9 | ## Feature Suggestion 10 | 11 | 12 | 13 | ## Context 14 | 15 | 16 | 17 | 18 | ## Possible Implementation 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/web/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"], 18 | "exclude": [ 19 | "node_modules", 20 | "**/*.test.ts", 21 | "**/*.test.tsx", 22 | "**/*.stories.tsx" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/web/nextjs/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MuiDecorator } from "../src/mui"; 3 | import * as nextImage from "next/image"; 4 | import "highlight.js/styles/github.css"; 5 | import "../src/components/Markdown/hljs.css" 6 | 7 | // Fix to make next/image work in Storybook (thanks https://stackoverflow.com/questions/64622746/how-to-mock-next-js-image-component-in-storybook) 8 | Object.defineProperty(nextImage, "default", { 9 | configurable: true, 10 | value: (props) => { 11 | return ; 12 | } 13 | }); 14 | 15 | export const decorators = [ 16 | (Story) => ( 17 | 18 | 19 | 20 | ) 21 | ]; 22 | -------------------------------------------------------------------------------- /.github/workflows/reusable-quality-checks.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | 4 | defaults: 5 | run: 6 | shell: bash 7 | 8 | jobs: 9 | quality-checks: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version-file: .nvmrc # current LTS 16 | cache: yarn 17 | cache-dependency-path: yarn.lock 18 | - run: yarn install --frozen-lockfile 19 | 20 | # TODO: make dev & test work without having to build everything (inspiration: https://github.com/Izhaki/mono.ts) 21 | # - run: yarn typescript 22 | - run: yarn build 23 | - run: yarn format 24 | - run: yarn lint 25 | -------------------------------------------------------------------------------- /packages/global-cli/src/log4brains: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require("path"); 3 | const { spawn } = require("child_process"); 4 | 5 | // Temporary fix of https://github.com/thomvaill/log4brains/issues/85 (legacy md4 hash used by webpack4) 6 | // that enables us to force the --openssl-legacy-provider NODE_OPTIONS flag without asking the user to do it himself. 7 | // This is really hacky but will be removed as soon as we manage to upgrade our dependencies. 8 | const child = spawn( 9 | "node", 10 | [path.join(__dirname, "index.js"), ...process.argv.slice(2)], 11 | { 12 | env: { ...process.env, NODE_OPTIONS: "--openssl-legacy-provider" }, 13 | cwd: process.cwd(), 14 | stdio: "inherit" 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/core-api/getIndexPageMarkdown.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { promises as fsP } from "fs"; 3 | import { getLog4brainsInstance } from "./instance"; 4 | 5 | export async function getIndexPageMarkdown(): Promise { 6 | const instance = getLog4brainsInstance(); 7 | const indexPath = path.join( 8 | instance.workdir, 9 | instance.config.project.adrFolder, 10 | "index.md" 11 | ); 12 | 13 | try { 14 | return await fsP.readFile(indexPath, { 15 | encoding: "utf8" 16 | }); 17 | } catch (e) { 18 | return `# Architecture knowledge base 19 | 20 | Please create \`${instance.config.project.adrFolder}/index.md\` to customize this homepage. 21 | `; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/core-api/instance.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Log4brains } from "@log4brains/core"; 3 | import { getConfig } from "../next"; 4 | 5 | let instance: Log4brains; 6 | 7 | export function getLog4brainsInstance(): Log4brains { 8 | if (!instance) { 9 | if (process.env.LOG4BRAINS_PHASE === "initial-build") { 10 | // Noop instance during "next build" phase 11 | instance = Log4brains.createFromCwd( 12 | path.join( 13 | getConfig().serverRuntimeConfig.PROJECT_ROOT, 14 | "lib/core-api/noop" 15 | ) 16 | ); 17 | } else { 18 | instance = Log4brains.createFromCwd(process.env.LOG4BRAINS_CWD || "."); 19 | } 20 | } 21 | return instance; 22 | } 23 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/toc-utils/Toc.ts: -------------------------------------------------------------------------------- 1 | import { TocContainer } from "./TocContainer"; 2 | import { TocSection } from "./TocSection"; 3 | 4 | export class Toc implements TocContainer { 5 | public readonly parent = null; 6 | 7 | readonly children: TocSection[] = []; 8 | 9 | createChild(title: string, id: string): TocSection { 10 | const child = new TocSection(this, title, id); 11 | this.children.push(child); 12 | return child; 13 | } 14 | 15 | // eslint-disable-next-line class-methods-use-this 16 | getLevel(): number { 17 | return 0; 18 | } 19 | 20 | render(renderer: (title: string, id: string, children: T[]) => T): T[] { 21 | return this.children.map((child) => child.render(renderer)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/docs/adr/20201002-use-explicit-architecture-and-ddd-for-the-core-api.md: -------------------------------------------------------------------------------- 1 | # Use Explicit Architecture and DDD for the core API 2 | 3 | - Status: accepted 4 | - Date: 2020-10-02 5 | 6 | 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. 7 | 8 | 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/). 9 | We will use this architecture for our core package. 10 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 5 | 6 | const deepFreezeRecur = (obj: any): any => { 7 | if (typeof obj !== "object") { 8 | return obj; 9 | } 10 | Object.keys(obj).forEach((prop) => { 11 | if (typeof obj[prop] === "object" && !Object.isFrozen(obj[prop])) { 12 | deepFreezeRecur(obj[prop]); 13 | } 14 | }); 15 | return Object.freeze(obj); 16 | }; 17 | 18 | /** 19 | * Apply Object.freeze() recursively on the given object and sub-objects. 20 | */ 21 | export const deepFreeze = (obj: T): T => { 22 | return deepFreezeRecur(obj); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetStaticProps } from "next"; 2 | import { getIndexPageMarkdown, getLog4brainsInstance } from "../lib/core-api"; 3 | import { getConfig } from "../lib/next"; 4 | import { IndexScene, IndexSceneProps } from "../scenes"; 5 | import { toAdrLight } from "../lib-shared/types"; 6 | 7 | export default IndexScene; 8 | 9 | export const getStaticProps: GetStaticProps = async () => { 10 | return { 11 | props: { 12 | projectName: getLog4brainsInstance().config.project.name, 13 | adrs: (await getLog4brainsInstance().searchAdrs()).map(toAdrLight), // For a faster 1st load and SEO 14 | markdown: await getIndexPageMarkdown(), 15 | l4bVersion: getConfig().serverRuntimeConfig.VERSION 16 | }, 17 | revalidate: 1 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrRelation.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from "@src/domain"; 2 | import type { Adr } from "./Adr"; 3 | import { MarkdownAdrLink } from "./MarkdownAdrLink"; 4 | 5 | type Props = { 6 | from: Adr; 7 | relation: string; 8 | to: Adr; 9 | }; 10 | 11 | export class AdrRelation extends ValueObject { 12 | constructor(from: Adr, relation: string, to: Adr) { 13 | super({ from, relation, to }); 14 | } 15 | 16 | get from(): Adr { 17 | return this.props.from; 18 | } 19 | 20 | get relation(): string { 21 | return this.props.relation; 22 | } 23 | 24 | get to(): Adr { 25 | return this.props.to; 26 | } 27 | 28 | toMarkdown(): string { 29 | const link = new MarkdownAdrLink(this.from, this.to); 30 | return `${this.relation} ${link.toMarkdown()}`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/buses/QueryBus.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import { Query, QueryHandler } from "@src/application"; 3 | 4 | export class QueryBus { 5 | private readonly handlersByQueryName: Map = new Map< 6 | string, 7 | QueryHandler 8 | >(); 9 | 10 | registerHandler(handler: QueryHandler, queryClass: Function): void { 11 | this.handlersByQueryName.set(queryClass.name, handler); 12 | } 13 | 14 | async dispatch(query: Query): Promise { 15 | const queryName = query.constructor.name; 16 | const handler = this.handlersByQueryName.get(queryName); 17 | if (!handler) { 18 | throw new Error(`No handler registered for this query: ${queryName}`); 19 | } 20 | return handler.execute(query) as Promise; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/reusable-get-log4brains-version.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: 4 | version: 5 | description: "Log4brains current version" 6 | value: ${{ jobs.get-log4brains-version.outputs.version }} 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | jobs: 13 | get-log4brains-version: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | version: ${{ steps.get-version.outputs.version }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | sparse-checkout: | 21 | lerna.json 22 | sparse-checkout-cone-mode: false 23 | - name: Get log4brains version from lerna.json 24 | id: get-version 25 | run: | 26 | version="$(cat lerna.json | jq -r .version)" 27 | echo "version=${version}" >> "${GITHUB_OUTPUT}" 28 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/buses/CommandBus.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import { Command, CommandHandler } from "@src/application"; 3 | 4 | export class CommandBus { 5 | private readonly handlersByCommandName: Map = new Map< 6 | string, 7 | CommandHandler 8 | >(); 9 | 10 | registerHandler(handler: CommandHandler, commandClass: Function): void { 11 | this.handlersByCommandName.set(commandClass.name, handler); 12 | } 13 | 14 | async dispatch(command: Command): Promise { 15 | const commandName = command.constructor.name; 16 | const handler = this.handlersByCommandName.get(commandName); 17 | if (!handler) { 18 | throw new Error(`No handler registered for this command: ${commandName}`); 19 | } 20 | return handler.execute(command); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/web/nextjs/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | module.exports = { 4 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 5 | addons: ["@storybook/addon-links", "@storybook/addon-essentials"], 6 | 7 | // Fix to make next/image work in Storybook (thanks https://stackoverflow.com/questions/64622746/how-to-mock-next-js-image-component-in-storybook) 8 | webpackFinal: (config) => { 9 | config.plugins.push( 10 | new webpack.DefinePlugin({ 11 | "process.env.__NEXT_IMAGE_OPTS": JSON.stringify({ 12 | deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], 13 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], 14 | domains: [], 15 | path: "/", 16 | loader: "default" 17 | }) 18 | }) 19 | ); 20 | return config; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/query-handlers/GenerateAdrSlugFromTitleCommandHandler.ts: -------------------------------------------------------------------------------- 1 | import { AdrSlug } from "@src/adr/domain"; 2 | import { QueryHandler } from "@src/application"; 3 | import { GenerateAdrSlugFromTitleQuery } from "../queries"; 4 | import { AdrRepository } from "../repositories"; 5 | 6 | type Deps = { 7 | adrRepository: AdrRepository; 8 | }; 9 | 10 | export class GenerateAdrSlugFromTitleQueryHandler implements QueryHandler { 11 | readonly queryClass = GenerateAdrSlugFromTitleQuery; 12 | 13 | private readonly adrRepository: AdrRepository; 14 | 15 | constructor({ adrRepository }: Deps) { 16 | this.adrRepository = adrRepository; 17 | } 18 | 19 | execute(query: GenerateAdrSlugFromTitleQuery): Promise { 20 | return Promise.resolve( 21 | this.adrRepository.generateAvailableSlug(query.title, query.packageRef) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/auto-issue-templates/new-node-lts.md: -------------------------------------------------------------------------------- 1 | # New Node.js LTS version detected 2 | 3 | _Issue created automatically by a Github action._ 4 | 5 | A new active Node.js LTS version has been detected. 6 | 7 | Please test the Log4brains project against this version and update the supported versions accordingly. 8 | 9 | - [ ] Update the LTS version in `.nvmrc` 10 | - [ ] Run `nvm use` and ensure `node -v` returns the new LTS version 11 | - [ ] Run `rm -rf node_modules && yarn install` 12 | - [ ] Run all the quality checks described in CONTRIBUTNG.md 13 | - [ ] Run and test the app manually 14 | - [ ] Fix the potential bugs 15 | - [ ] Update `.github/supported_nodejs_versions.json`: add the new LTS version and remove the ones that are no longer supported (see [Node.js release schedule](https://github.com/nodejs/release#release-schedule)) 16 | - [ ] Update all `engine.node` fields in `package.json` files 17 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/MarkdownAdrLink.ts: -------------------------------------------------------------------------------- 1 | import { Log4brainsError, ValueObject } from "@src/domain"; 2 | import type { Adr } from "./Adr"; 3 | 4 | type Props = { 5 | from: Adr; 6 | to: Adr; 7 | }; 8 | 9 | export class MarkdownAdrLink extends ValueObject { 10 | constructor(from: Adr, to: Adr) { 11 | super({ from, to }); 12 | } 13 | 14 | get from(): Adr { 15 | return this.props.from; 16 | } 17 | 18 | get to(): Adr { 19 | return this.props.to; 20 | } 21 | 22 | toMarkdown(): string { 23 | if (!this.from.file || !this.to.file) { 24 | throw new Log4brainsError( 25 | "Impossible to create a link between two unsaved ADRs", 26 | `${this.from.slug.value} -> ${this.to.slug.value}` 27 | ); 28 | } 29 | const relativePath = this.from.file.path.relative(this.to.file.path); 30 | return `[${this.to.slug.value}](${relativePath})`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/docs/adr/20201003-markdown-parsing-is-part-of-the-domain.md: -------------------------------------------------------------------------------- 1 | # Markdown parsing is part of the domain 2 | 3 | - Status: accepted 4 | - Date: 2020-10-03 5 | 6 | ## Context and Problem Statement 7 | 8 | Development of the core domain. 9 | 10 | ## Considered Options 11 | 12 | - Markdown is part of the domain 13 | - Markdown is a technical detail, which should be developed in the infrastructure layer 14 | 15 | ## Decision Outcome 16 | 17 | Chosen option: "Markdown is part of the domain" because we want to be able to parse it "smartly", without forcing a specific structure. 18 | Therefore, a lot of business logic is involved and should be tested. 19 | 20 | ### Positive Consequences 21 | 22 | - Test coverage of the markdown parsing 23 | 24 | ### Negative Consequences 25 | 26 | - The business logic is tightly tied to the markdown format. It won't be possible to switch to another format easily in the future 27 | -------------------------------------------------------------------------------- /packages/core/src/domain/ValueObject.ts: -------------------------------------------------------------------------------- 1 | import isEqual from "lodash/isEqual"; 2 | 3 | // Inspired from https://khalilstemmler.com/articles/typescript-value-object/ 4 | // Thank you :-) 5 | 6 | export type ValueObjectProps = Record; 7 | 8 | /** 9 | * @desc ValueObjects are objects that we determine their 10 | * equality through their structural property. 11 | */ 12 | export abstract class ValueObject { 13 | public readonly props: T; 14 | 15 | constructor(props: T) { 16 | this.props = Object.freeze(props); 17 | } 18 | 19 | public equals(vo?: ValueObject): boolean { 20 | if (vo === null || vo === undefined) { 21 | return false; 22 | } 23 | if (vo.constructor.name !== this.constructor.name) { 24 | return false; 25 | } 26 | if (vo.props === undefined) { 27 | return false; 28 | } 29 | return isEqual(this.props, vo.props); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/init/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsP } from "fs"; 2 | 3 | function escapeRegExp(str: string) { 4 | return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string 5 | } 6 | 7 | /** 8 | * string.replaceAll() polyfill 9 | * TODO: remove when support down to Node 15 10 | * @param str 11 | * @param search 12 | * @param replacement 13 | */ 14 | function replaceAll(str: string, search: string, replacement: string): string { 15 | return str.replace(new RegExp(escapeRegExp(search), "g"), replacement); 16 | } 17 | 18 | export async function replaceAllInFile( 19 | path: string, 20 | replacements: [string, string][] 21 | ): Promise { 22 | let content = await fsP.readFile(path, "utf-8"); 23 | content = replacements.reduce((prevContent, replacement) => { 24 | return replaceAll(prevContent, replacement[0], replacement[1]); 25 | }, content); 26 | await fsP.writeFile(path, content, "utf-8"); 27 | } 28 | -------------------------------------------------------------------------------- /packages/init/src/cli.ts: -------------------------------------------------------------------------------- 1 | import commander from "commander"; 2 | import type { AppConsole } from "@log4brains/cli-common"; 3 | import { InitCommand, InitCommandOpts } from "./commands"; 4 | 5 | type Deps = { 6 | appConsole: AppConsole; 7 | }; 8 | 9 | export function createInitCli({ appConsole }: Deps): commander.Command { 10 | const program = new commander.Command(); 11 | 12 | program 13 | .command("init") 14 | .arguments("[path]") 15 | .description("Configures Log4brains for your project", { 16 | path: "Path of your project. Default: current directory" 17 | }) 18 | .option( 19 | "-d, --defaults", 20 | "Run in non-interactive mode and use the common default options", 21 | false 22 | ) 23 | .action( 24 | (path: string | undefined, options: InitCommandOpts): Promise => { 25 | return new InitCommand({ appConsole }).execute(options, path); 26 | } 27 | ); 28 | 29 | return program; 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/reusable-wait-for-npm-version-to-be-published.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | npm-package: 5 | required: true 6 | type: string 7 | npm-version: # only exact versions allowed, not tags like "latest" or "alpha" 8 | required: true 9 | type: string 10 | wait-minutes: 11 | required: false 12 | type: string 13 | default: "10" 14 | 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | jobs: 20 | wait-for-npm-version-to-be-published: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/setup-node@v4 24 | - uses: actions/checkout@v4 25 | with: 26 | sparse-checkout: | 27 | scripts/wait-for-npm-version-to-be-published.sh 28 | sparse-checkout-cone-mode: false 29 | - name: Wait 30 | run: ./scripts/wait-for-npm-version-to-be-published.sh "${{ inputs.npm-package }}" "${{ inputs.npm-version }}" "${{ inputs.wait-minutes }}" 31 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/command-handlers/SupersedeAdrCommandHandler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler } from "@src/application"; 2 | import { SupersedeAdrCommand } from "../commands"; 3 | import { AdrRepository } from "../repositories"; 4 | 5 | type Deps = { 6 | adrRepository: AdrRepository; 7 | }; 8 | 9 | export class SupersedeAdrCommandHandler implements CommandHandler { 10 | readonly commandClass = SupersedeAdrCommand; 11 | 12 | private readonly adrRepository: AdrRepository; 13 | 14 | constructor({ adrRepository }: Deps) { 15 | this.adrRepository = adrRepository; 16 | } 17 | 18 | async execute(command: SupersedeAdrCommand): Promise { 19 | const supersededAdr = await this.adrRepository.find(command.supersededSlug); 20 | const supersederAdr = await this.adrRepository.find(command.supersederSlug); 21 | supersededAdr.supersedeBy(supersederAdr); 22 | await this.adrRepository.save(supersededAdr); 23 | await this.adrRepository.save(supersederAdr); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/search/instance.ts: -------------------------------------------------------------------------------- 1 | import Router from "next/router"; 2 | import { Log4brainsMode } from "../../contexts"; 3 | import { Search, SerializedIndex } from "../../lib-shared/search"; 4 | 5 | function isSerializedIndex(obj: unknown): obj is SerializedIndex { 6 | return ( 7 | typeof obj === "object" && 8 | obj !== null && 9 | "lunr" in obj && 10 | "adrs" in obj && 11 | Array.isArray((obj as SerializedIndex).adrs) 12 | ); 13 | } 14 | 15 | export async function createSearchInstance( 16 | mode: Log4brainsMode 17 | ): Promise { 18 | const index = (await ( 19 | await fetch( 20 | mode === Log4brainsMode.preview 21 | ? `/api/search-index` 22 | : `${Router.basePath}/data/${process.env.NEXT_BUILD_ID}/search-index.json` 23 | ) 24 | ).json()) as unknown; 25 | if (!isSerializedIndex(index)) { 26 | throw new Error(`Invalid Search SerializedIndex: ${index}`); 27 | } 28 | return Search.createFromSerializedIndex(index); 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/reusable-load-nodejs-supported-versions.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: 4 | node_versions: 5 | description: "Node.js versions officially supported by the project" 6 | value: ${{ jobs.load-nodejs-supported-versions.outputs.node_versions }} 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | jobs: 13 | load-nodejs-supported-versions: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | node_versions: ${{ steps.get-versions.outputs.node_versions }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | sparse-checkout: | 21 | .github/supported_nodejs_versions.json 22 | sparse-checkout-cone-mode: false 23 | - name: Get supported Node.js versions from JSON file 24 | id: get-versions 25 | run: | 26 | node_versions="$(cat .github/supported_nodejs_versions.json | jq -r '.versions | @json')" 27 | echo "node_versions=${node_versions}" >> "${GITHUB_OUTPUT}" 28 | -------------------------------------------------------------------------------- /packages/init/assets/use-log4brains-to-manage-the-adrs.md: -------------------------------------------------------------------------------- 1 | # Use Log4brains to manage the ADRs 2 | 3 | - Status: accepted 4 | - Date: {DATE_YESTERDAY} 5 | - Tags: dev-tools, doc 6 | 7 | ## Context and Problem Statement 8 | 9 | We want to record architectural decisions made in this project. 10 | Which tool(s) should we use to manage these records? 11 | 12 | ## Considered Options 13 | 14 | - [Log4brains](https://github.com/thomvaill/log4brains): architecture knowledge base (command-line + static site generator) 15 | - [ADR Tools](https://github.com/npryce/adr-tools): command-line to create ADRs 16 | - [ADR Tools Python](https://bitbucket.org/tinkerer_/adr-tools-python/src/master/): command-line to create ADRs 17 | - [adr-viewer](https://github.com/mrwilson/adr-viewer): static site generator 18 | - [adr-log](https://adr.github.io/adr-log/): command-line to create a TOC of ADRs 19 | 20 | ## Decision Outcome 21 | 22 | Chosen option: "Log4brains", because it includes the features of all the other tools, and even more. 23 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/AdrStatusChip/AdrStatusChip.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Meta, Story } from "@storybook/react"; 3 | import { AdrStatusChip, AdrStatusChipProps } from "./AdrStatusChip"; 4 | 5 | const Template: Story = (args) => ( 6 | 7 | ); 8 | 9 | export default { 10 | title: "AdrStatusChip", 11 | component: AdrStatusChip 12 | } as Meta; 13 | 14 | export const Draft = Template.bind({}); 15 | Draft.args = { status: "draft" }; 16 | 17 | export const Proposed = Template.bind({}); 18 | Proposed.args = { status: "proposed" }; 19 | 20 | export const Rejected = Template.bind({}); 21 | Rejected.args = { status: "rejected" }; 22 | 23 | export const Accepted = Template.bind({}); 24 | Accepted.args = { status: "accepted" }; 25 | 26 | export const Deprecated = Template.bind({}); 27 | Deprecated.args = { status: "deprecated" }; 28 | 29 | export const Superseded = Template.bind({}); 30 | Superseded.args = { status: "superseded" }; 31 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/next.ts: -------------------------------------------------------------------------------- 1 | import getNextConfig from "next/config"; 2 | 3 | export type L4bNextConfig = { 4 | serverRuntimeConfig: { 5 | PROJECT_ROOT: string; 6 | VERSION: string; 7 | }; 8 | }; 9 | 10 | function isObjectWithGivenProperties( 11 | obj: unknown, 12 | properties: K[] 13 | ): obj is Record { 14 | return ( 15 | typeof obj === "object" && 16 | obj !== null && 17 | properties.every((property) => property in obj) 18 | ); 19 | } 20 | 21 | function isL4bNextConfig(config: unknown): config is L4bNextConfig { 22 | return ( 23 | isObjectWithGivenProperties(config, ["serverRuntimeConfig"]) && 24 | isObjectWithGivenProperties(config.serverRuntimeConfig, ["PROJECT_ROOT"]) 25 | ); 26 | } 27 | 28 | export function getConfig(): L4bNextConfig { 29 | const config = getNextConfig() as unknown; 30 | if (!isL4bNextConfig(config)) { 31 | throw new Error(`Invalid Next.js config object: ${config}`); 32 | } 33 | return config; 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/lib/cheerio-markdown/CheerioMarkdownElement.ts: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio"; 2 | 3 | export class CheerioMarkdownElement { 4 | constructor(private readonly cheerioElt: cheerio.Cheerio) {} 5 | 6 | get startLine(): number | undefined { 7 | const data = this.cheerioElt.data("sourceLineStart") as string | undefined; 8 | return data !== undefined ? parseInt(data, 10) : undefined; 9 | } 10 | 11 | get endLine(): number | undefined { 12 | const data = this.cheerioElt.data("sourceLineEnd") as string | undefined; 13 | return data !== undefined ? parseInt(data, 10) : undefined; 14 | } 15 | 16 | get markup(): string | undefined { 17 | const data = this.cheerioElt.data("sourceMarkup") as string | undefined; 18 | return data !== undefined ? data : undefined; 19 | } 20 | 21 | get level(): number | undefined { 22 | const data = this.cheerioElt.data("sourceLevel") as string | undefined; 23 | return data !== undefined ? parseInt(data, 10) : undefined; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.ts: -------------------------------------------------------------------------------- 1 | import { Adr } from "@src/adr/domain"; 2 | import { QueryHandler } from "@src/application"; 3 | import { ValueObjectArray } from "@src/domain"; 4 | import { SearchAdrsQuery } from "../queries"; 5 | import { AdrRepository } from "../repositories"; 6 | 7 | type Deps = { 8 | adrRepository: AdrRepository; 9 | }; 10 | 11 | export class SearchAdrsQueryHandler implements QueryHandler { 12 | readonly queryClass = SearchAdrsQuery; 13 | 14 | private readonly adrRepository: AdrRepository; 15 | 16 | constructor({ adrRepository }: Deps) { 17 | this.adrRepository = adrRepository; 18 | } 19 | 20 | async execute(query: SearchAdrsQuery): Promise { 21 | return (await this.adrRepository.findAll()).filter((adr) => { 22 | if ( 23 | query.filters.statuses && 24 | !ValueObjectArray.inArray(adr.status, query.filters.statuses) 25 | ) { 26 | return false; 27 | } 28 | return true; 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/docs/adr/20201027-adr-link-resolver-in-the-domain.md: -------------------------------------------------------------------------------- 1 | # ADR link resolver in the domain 2 | 3 | - Status: accepted 4 | - Date: 2020-10-27 5 | 6 | ## Context and Problem Statement 7 | 8 | We have to translate markdown links between ADRs to static site links. 9 | We cannot deduce the slug easily from the paths because of the "path / package" mapping, which is only known from the config. 10 | 11 | ## Considered Options 12 | 13 | - Option 1: the ADR repository sets a Map on the ADR (`path -> slug`) for every link discovered in the Markdown 14 | - Option 2: we introduce an "ADR link resolver" in the domain 15 | - Option 3: we don't rely on link paths, but only on link labels (ie label must === slug) 16 | 17 | ## Decision Outcome 18 | 19 | Chosen option: "Option 2: we introduce an "ADR link resolver" in the domain". 20 | Because option 3 is too restrictive and option 1 seems too hacky. 21 | And this solution is compatible with [20201003-markdown-parsing-is-part-of-the-domain](20201003-markdown-parsing-is-part-of-the-domain.md). 22 | -------------------------------------------------------------------------------- /docs/adr/README.md: -------------------------------------------------------------------------------- 1 | # Architecture Decision Records 2 | 3 | ADRs are automatically published to our Log4brains architecture knowledge base: 4 | 5 | 🔗 **** 6 | 7 | Please use this link to browse them. 8 | 9 | ## Development 10 | 11 | If not already done, install Log4brains: 12 | 13 | ```bash 14 | npm install -g log4brains 15 | ``` 16 | 17 | To preview the knowledge base locally, run: 18 | 19 | ```bash 20 | log4brains preview 21 | ``` 22 | 23 | In preview mode, the Hot Reload feature is enabled: any change you make to a markdown file is applied live in the UI. 24 | 25 | To create a new ADR interactively, run: 26 | 27 | ```bash 28 | log4brains adr new 29 | ``` 30 | 31 | ## More information 32 | 33 | - [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/develop#readme) 34 | - [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/develop#-what-is-an-adr-and-why-should-you-use-them) 35 | - [ADR GitHub organization](https://adr.github.io/) 36 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/query-handlers/GetAdrBySlugQueryHandler.ts: -------------------------------------------------------------------------------- 1 | import { Adr } from "@src/adr/domain"; 2 | import { QueryHandler } from "@src/application"; 3 | import { Log4brainsError } from "@src/domain"; 4 | import { GetAdrBySlugQuery } from "../queries"; 5 | import { AdrRepository } from "../repositories"; 6 | 7 | type Deps = { 8 | adrRepository: AdrRepository; 9 | }; 10 | 11 | export class GetAdrBySlugQueryHandler implements QueryHandler { 12 | readonly queryClass = GetAdrBySlugQuery; 13 | 14 | private readonly adrRepository: AdrRepository; 15 | 16 | constructor({ adrRepository }: Deps) { 17 | this.adrRepository = adrRepository; 18 | } 19 | 20 | async execute(query: GetAdrBySlugQuery): Promise { 21 | try { 22 | return await this.adrRepository.find(query.slug); 23 | } catch (e) { 24 | if ( 25 | !(e instanceof Log4brainsError && e.name === "This ADR does not exist") 26 | ) { 27 | throw e; 28 | } 29 | } 30 | return undefined; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/init/assets/README.md: -------------------------------------------------------------------------------- 1 | # Architecture Decision Records 2 | 3 | ADRs are automatically published to our Log4brains architecture knowledge base: 4 | 5 | 🔗 **** 6 | 7 | Please use this link to browse them. 8 | 9 | ## Development 10 | 11 | If not already done, install Log4brains: 12 | 13 | ```bash 14 | npm install -g log4brains 15 | ``` 16 | 17 | To preview the knowledge base locally, run: 18 | 19 | ```bash 20 | log4brains preview 21 | ``` 22 | 23 | In preview mode, the Hot Reload feature is enabled: any change you make to a markdown file is applied live in the UI. 24 | 25 | To create a new ADR interactively, run: 26 | 27 | ```bash 28 | log4brains adr new 29 | ``` 30 | 31 | ## More information 32 | 33 | - [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/develop#readme) 34 | - [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/develop#-what-is-an-adr-and-why-should-you-use-them) 35 | - [ADR GitHub organization](https://adr.github.io/) 36 | -------------------------------------------------------------------------------- /docs/adr/20200926-use-the-adr-number-as-its-unique-id.md: -------------------------------------------------------------------------------- 1 | # Use the ADR number as its unique ID 2 | 3 | - Status: superseded by [20201016-use-the-adr-slug-as-its-unique-id](20201016-use-the-adr-slug-as-its-unique-id.md) 4 | - Date: 2020-09-26 5 | 6 | ## Context and Problem Statement 7 | 8 | We need to be able to identify uniquely an ADR, especially in these contexts: 9 | 10 | - Web: to build its URL 11 | - CLI: to identify an ADR in a command argument (example: "edit", or "preview") 12 | 13 | ## Considered Options 14 | 15 | - ADR number (ie. filename prefixed number, example: `0001-use-markdown-architectural-decision-records.md`) 16 | - ADR filename 17 | - ADR title 18 | 19 | ## Decision Outcome 20 | 21 | Chosen option: "ADR number", because 22 | 23 | - It is possible to have duplicated titles 24 | - 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 25 | - Other ADR tools like [adr-tools](https://github.com/npryce/adr-tools) already use the number as a unique ID 26 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/toc-utils/TocSection.ts: -------------------------------------------------------------------------------- 1 | import { TocContainer } from "./TocContainer"; 2 | 3 | export class TocSection { 4 | readonly children: TocSection[] = []; 5 | 6 | readonly parent: TocContainer; 7 | 8 | readonly title: string; 9 | 10 | readonly id: string; 11 | 12 | // Typescript parameter properties are not supported by Storybook for now! :-( 13 | // https://github.com/storybookjs/storybook/issues/12019 14 | constructor(parent: TocContainer, title: string, id: string) { 15 | this.parent = parent; 16 | this.title = title; 17 | this.id = id; 18 | } 19 | 20 | createChild(title: string, id: string): TocSection { 21 | const child = new TocSection(this, title, id); 22 | this.children.push(child); 23 | return child; 24 | } 25 | 26 | getLevel(): number { 27 | return this.parent.getLevel() + 1; 28 | } 29 | 30 | render(renderer: (title: string, id: string, children: T[]) => T): T { 31 | const c = this.children.map((child) => child.render(renderer)); 32 | return renderer(this.title, this.id, c); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-checks.yml: -------------------------------------------------------------------------------- 1 | # Inspired from https://github.com/backstage/backstage/blob/master/.github/workflows/ci.yml. Thanks! 2 | name: Pull request checks 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | jobs: 12 | quality-checks: 13 | uses: ./.github/workflows/reusable-quality-checks.yml 14 | 15 | # We only test the LTS on all OSes for performance reasons 16 | # Other versions will be tested on the main workflow after the merge 17 | tests-LTS-per-os: 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, windows-latest, macos-latest] 21 | uses: ./.github/workflows/reusable-tests.yml 22 | with: 23 | only-changed-packages: true # for performance 24 | os: ${{ matrix.os }} 25 | node-version-file: .nvmrc # current LTS 26 | 27 | tests-nodejs-current: 28 | uses: ./.github/workflows/reusable-tests.yml 29 | with: 30 | only-changed-packages: true # for performance 31 | os: ubuntu-latest 32 | node-version: current 33 | experimental: true # best effort mode 34 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/AdrBrowserLayout/AdrBrowserLayout.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Meta, Story } from "@storybook/react"; 3 | import { AdrBrowserLayout, AdrBrowserLayoutProps } from ".."; 4 | import { adrMocks } from "../../../.storybook/mocks"; 5 | import { toAdrLight } from "../../lib-shared/types"; 6 | 7 | const Template: Story = (args) => ( 8 | 9 | ); 10 | 11 | export default { 12 | title: "Layouts/AdrBrowser", 13 | component: AdrBrowserLayout 14 | } as Meta; 15 | 16 | export const Default = Template.bind({}); 17 | Default.args = { adrs: adrMocks.map(toAdrLight) }; 18 | 19 | export const LoadingMenu = Template.bind({}); 20 | LoadingMenu.args = {}; 21 | 22 | export const ReloadingMenu = Template.bind({}); 23 | ReloadingMenu.args = { adrs: adrMocks.map(toAdrLight), adrsReloading: true }; 24 | 25 | export const EmptyMenu = Template.bind({}); 26 | EmptyMenu.args = { adrs: [] }; 27 | 28 | export const RoutingProgressBar = Template.bind({}); 29 | RoutingProgressBar.args = { adrs: adrMocks.map(toAdrLight), routing: true }; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug Report" 3 | about: "Report a bug" 4 | labels: bug 5 | --- 6 | 7 | # Bug Report 8 | 9 | ## Description 10 | 11 | 12 | 13 | ## Steps to Reproduce 14 | 15 | 16 | 17 | 18 | 19 | 1. Step 1 20 | 2. Step 2 21 | 3. ... 22 | 23 | ## Expected Behavior 24 | 25 | 26 | 27 | ## Context 28 | 29 | 30 | 31 | 32 | ## Environment 33 | 34 | 35 | 36 | - Log4brains version: 37 | - Node.js version: 38 | - OS and its version: 39 | - Browser information: 40 | 41 | ## Possible Solution 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/core/src/lib/cheerio-markdown/markdown-it-source-map-plugin.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | /* eslint-disable no-param-reassign */ 3 | import MarkdownIt from "markdown-it"; 4 | 5 | // Source: https://github.com/tylingsoft/markdown-it-source-map 6 | // Thanks! ;) 7 | // Had to fork it to add additional information 8 | 9 | export function markdownItSourceMap(md: MarkdownIt): void { 10 | const defaultRenderToken = md.renderer.renderToken.bind(md.renderer); 11 | md.renderer.renderToken = function (tokens, idx, options) { 12 | const token = tokens[idx]; 13 | if (token.type.endsWith("_open")) { 14 | if (token.map) { 15 | token.attrPush(["data-source-line-start", token.map[0].toString()]); 16 | token.attrPush(["data-source-line-end", token.map[1].toString()]); 17 | } 18 | if (token.markup !== undefined) { 19 | token.attrPush(["data-source-markup", token.markup]); 20 | } 21 | if (token.level !== undefined) { 22 | token.attrPush(["data-source-level", token.level.toString()]); 23 | } 24 | } 25 | return defaultRenderToken(tokens, idx, options); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrFile.test.ts: -------------------------------------------------------------------------------- 1 | import { AdrFile } from "./AdrFile"; 2 | import { AdrSlug } from "./AdrSlug"; 3 | import { FilesystemPath } from "./FilesystemPath"; 4 | 5 | describe("AdrFile", () => { 6 | it("throws when not .md", () => { 7 | expect(() => { 8 | new AdrFile(new FilesystemPath("/", "test")); 9 | }).toThrow(); 10 | }); 11 | 12 | it("throws when reserved filename", () => { 13 | expect(() => { 14 | new AdrFile(new FilesystemPath("/", "template.md")); 15 | }).toThrow(); 16 | expect(() => { 17 | new AdrFile(new FilesystemPath("/", "README.md")); 18 | }).toThrow(); 19 | expect(() => { 20 | new AdrFile(new FilesystemPath("/", "index.md")); 21 | }).toThrow(); 22 | expect(() => { 23 | new AdrFile(new FilesystemPath("/", "backlog.md")); 24 | }).toThrow(); 25 | }); 26 | 27 | it("creates from slug in folder", () => { 28 | expect( 29 | AdrFile.createFromSlugInFolder( 30 | new FilesystemPath("/", "test"), 31 | new AdrSlug("my-package/20200101-hello-world") 32 | ).path.absolutePath 33 | ).toEqual("/test/20200101-hello-world.md"); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /docs/adr/20201026-the-core-api-is-responsible-for-enhancing-the-adr-markdown-body-with-mdx.md: -------------------------------------------------------------------------------- 1 | # The core API is responsible for enhancing the ADR markdown body with MDX 2 | 3 | - Status: accepted 4 | - Date: 2020-10-26 5 | 6 | ## Context and Problem Statement 7 | 8 | The markdown body of ADRs cannot be used as is, because: 9 | 10 | - Links between ADRs have to be replaced with correct URLs 11 | - Header (status, date, deciders etc...) has to be rendered with specific components 12 | 13 | ## Decision Drivers 14 | 15 | - Potential future development of a VSCode extension 16 | 17 | ## Considered Options 18 | 19 | - Option 1: the UI is responsible 20 | - Option 2: the core API is responsible (with MDX) 21 | 22 | ## Decision Outcome 23 | 24 | Chosen option: "Option 2: the core API is responsible (with MDX)". 25 | Because if we develop the VSCode extension, it is better to add more business logic into the core package, and it is better tested. 26 | 27 | ### Positive Consequences 28 | 29 | - The metadata in the header is simply removed 30 | 31 | ### Negative Consequences 32 | 33 | - Each UI package will have to implement its own Header component 34 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @log4brains/core 2 | 3 | This package provides the core API of the [Log4brains](https://github.com/thomvaill/log4brains) architecture knowledge base. 4 | It is not meant to be used directly. 5 | 6 | Install [the main log4brains package](https://www.npmjs.com/package/log4brains) instead: 7 | 8 | ```bash 9 | npm install -g log4brains 10 | ``` 11 | 12 | ## Installation 13 | 14 | This package is not meant to be installed directly in your project. 15 | 16 | However, if you want to create a package that uses [Log4brains](https://github.com/thomvaill/log4brains)' API, 17 | you can include this package as a dependency: 18 | 19 | ```bash 20 | npm install --save @log4brains/core 21 | ``` 22 | 23 | or 24 | 25 | ```bash 26 | yarn add @log4brains/core 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```typescript 32 | import { Log4brains } from "@log4brains/core"; 33 | 34 | const l4b = Log4brains.createFromCwd(process.cwd()); 35 | 36 | // See the TypeDoc documentation (TODO: to deploy on GitHub pages) to see available API methods 37 | ``` 38 | 39 | ## Documentation 40 | 41 | - TypeDoc documentation (TODO) 42 | - [Log4brains README](https://github.com/thomvaill/log4brains/blob/develop/README.md) 43 | -------------------------------------------------------------------------------- /packages/core/src/adr/infrastructure/MarkdownAdrLinkResolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Adr, 3 | AdrFile, 4 | MarkdownAdrLink, 5 | MarkdownAdrLinkResolver as IMarkdownAdrLinkResolver 6 | } from "@src/adr/domain"; 7 | import { Log4brainsError } from "@src/domain"; 8 | import type { AdrRepository } from "./repositories"; 9 | 10 | type Deps = { 11 | adrRepository: AdrRepository; 12 | }; 13 | 14 | export class MarkdownAdrLinkResolver implements IMarkdownAdrLinkResolver { 15 | private readonly adrRepository: AdrRepository; 16 | 17 | constructor({ adrRepository }: Deps) { 18 | this.adrRepository = adrRepository; 19 | } 20 | 21 | async resolve(from: Adr, uri: string): Promise { 22 | if (!from.file) { 23 | throw new Log4brainsError( 24 | "Impossible to resolve links on an non-saved ADR" 25 | ); 26 | } 27 | 28 | const path = from.file.path.join("..").join(uri); 29 | if (!AdrFile.isPathValid(path)) { 30 | return undefined; 31 | } 32 | 33 | const to = await this.adrRepository.findFromFile(new AdrFile(path)); 34 | if (!to) { 35 | return undefined; 36 | } 37 | 38 | return new MarkdownAdrLink(from, to); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[typescript]": { 4 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 8 | }, 9 | "[javascript]": { 10 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 11 | }, 12 | "[javascriptreact]": { 13 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 14 | }, 15 | "[jsonc]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[json]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[markdown]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "cSpell.words": [ 25 | "Diagnosticable", 26 | "Transpiled", 27 | "adrs", 28 | "awilix", 29 | "clsx", 30 | "copyfiles", 31 | "diagnotics", 32 | "esnext", 33 | "execa", 34 | "gitlab", 35 | "globby", 36 | "htmlentities", 37 | "lunr", 38 | "microbundle", 39 | "neverthrow", 40 | "outdir", 41 | "signale", 42 | "typedoc", 43 | "unversioned", 44 | "workdir" 45 | ], 46 | "typescript.tsdk": "node_modules/typescript/lib" 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/MarkdownAdrLink.test.ts: -------------------------------------------------------------------------------- 1 | import { Adr } from "./Adr"; 2 | import { AdrFile } from "./AdrFile"; 3 | import { AdrSlug } from "./AdrSlug"; 4 | import { FilesystemPath } from "./FilesystemPath"; 5 | import { MarkdownAdrLink } from "./MarkdownAdrLink"; 6 | import { MarkdownBody } from "./MarkdownBody"; 7 | import { PackageRef } from "./PackageRef"; 8 | 9 | describe("MarkdownAdrLink", () => { 10 | beforeAll(() => { 11 | Adr.setTz("Etc/UTC"); 12 | }); 13 | afterAll(() => { 14 | Adr.clearTz(); 15 | }); 16 | 17 | it("works with relative paths", () => { 18 | const from = new Adr({ 19 | slug: new AdrSlug("from"), 20 | file: new AdrFile(new FilesystemPath("/", "docs/adr/from.md")), 21 | body: new MarkdownBody("") 22 | }); 23 | const to = new Adr({ 24 | slug: new AdrSlug("test/to"), 25 | package: new PackageRef("test"), 26 | file: new AdrFile( 27 | new FilesystemPath("/", "packages/test/docs/adr/to.md") 28 | ), 29 | body: new MarkdownBody("") 30 | }); 31 | const link = new MarkdownAdrLink(from, to); 32 | expect(link.toMarkdown()).toEqual( 33 | "[test/to](../../packages/test/docs/adr/to.md)" 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/AdrBrowserLayout/components/RoutingProgress/RoutingProgress.tsx: -------------------------------------------------------------------------------- 1 | import { LinearProgress } from "@material-ui/core"; 2 | import { makeStyles, createStyles } from "@material-ui/core/styles"; 3 | import React from "react"; 4 | 5 | const useStyles = makeStyles(() => 6 | createStyles({ 7 | root: { 8 | top: 0, 9 | width: "100%", 10 | height: 2, 11 | position: "absolute" 12 | } 13 | }) 14 | ); 15 | 16 | export function RoutingProgress() { 17 | const classes = useStyles(); 18 | const [progress, setProgress] = React.useState(0); 19 | 20 | React.useEffect(() => { 21 | const timer = setInterval(() => { 22 | setProgress((oldProgress) => { 23 | if (oldProgress > 99.999) { 24 | clearInterval(timer); 25 | return 100; 26 | } 27 | return oldProgress + (100 - oldProgress) / 8; 28 | }); 29 | }, 100); 30 | 31 | return () => { 32 | clearInterval(timer); 33 | }; 34 | }, []); 35 | 36 | return ( 37 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib-shared/types.ts: -------------------------------------------------------------------------------- 1 | import { AdrDto } from "@log4brains/core"; 2 | 3 | export type Adr = Omit & { 4 | supersededBy: AdrLight | null; 5 | body: { enhancedMdx: string }; 6 | }; 7 | 8 | export type AdrLight = Pick< 9 | Adr, 10 | "slug" | "package" | "title" | "status" | "creationDate" | "publicationDate" 11 | >; 12 | 13 | export function toAdrLight(adr: AdrDto | Adr | AdrLight): AdrLight { 14 | return { 15 | slug: adr.slug, 16 | package: adr.package, 17 | title: adr.title, 18 | status: adr.status, 19 | creationDate: adr.creationDate, 20 | publicationDate: adr.publicationDate 21 | }; 22 | } 23 | 24 | export function toAdr(dto: AdrDto, superseder?: AdrLight): Adr { 25 | if (dto.supersededBy && !superseder) { 26 | throw new Error("You forgot to pass the superseder"); 27 | } 28 | if (superseder && superseder.slug !== dto.supersededBy) { 29 | throw new Error( 30 | "The given superseder does not match the `supersededBy` field" 31 | ); 32 | } 33 | 34 | return { 35 | ...dto, 36 | supersededBy: superseder ? toAdrLight(superseder) : null, 37 | body: { 38 | enhancedMdx: dto.body.enhancedMdx 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/integration-tests/rw-project/docs/adr/template.md: -------------------------------------------------------------------------------- 1 | # [short title of solved problem and solution] 2 | 3 | - Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)] 4 | - Deciders: [list everyone involved in the decision] 5 | - Date: [YYYY-MM-DD when the decision was last updated] 6 | - Tags: [space and/or comma separated list of tags] 7 | 8 | Technical Story: [description | ticket/issue URL] 9 | 10 | ## Context and Problem Statement 11 | 12 | [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.] 13 | 14 | 15 | 16 | ## Decision Outcome 17 | 18 | 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)]. 19 | 20 | 21 | 22 | ## Links 23 | 24 | - [Link type][link to adr] 25 | - … 26 | -------------------------------------------------------------------------------- /packages/global-cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | import commander from "commander"; 2 | import type { AppConsole } from "@log4brains/cli-common"; 3 | import { createInitCli } from "@log4brains/init"; 4 | import { createCli } from "@log4brains/cli"; 5 | import { createWebCli } from "@log4brains/web"; 6 | 7 | type Deps = { 8 | appConsole: AppConsole; 9 | version: string; 10 | }; 11 | 12 | export function createGlobalCli({ 13 | appConsole, 14 | version 15 | }: Deps): commander.Command { 16 | const program = new commander.Command(); 17 | program 18 | .version(version) 19 | .description( 20 | "Log4brains CLI to preview and build your architecture knowledge base.\n" + 21 | "You can also manage your ADRs from here (see `log4brains adr --help`).\n\n" + 22 | "All the commands should be run from your project's root folder.\n\n" + 23 | "Add the `--help` option to any command to see its detailed documentation." 24 | ); 25 | 26 | const initCli = createInitCli({ appConsole }); 27 | const cli = createCli({ appConsole }); 28 | const webCli = createWebCli({ appConsole }); 29 | 30 | [...initCli.commands, ...cli.commands, ...webCli.commands].forEach((cmd) => { 31 | program.addCommand(cmd); 32 | }); 33 | 34 | return program; 35 | } 36 | -------------------------------------------------------------------------------- /docs/adr/20201103-use-lunr-for-search.md: -------------------------------------------------------------------------------- 1 | # Use Lunr for search 2 | 3 | - Status: accepted 4 | - Date: 2020-11-03 5 | 6 | ## Context and Problem Statement 7 | 8 | We have to provide a search bar to perform full-text search on ADRs. 9 | 10 | ## Decision Drivers 11 | 12 | - Works in preview mode AND in the statically built version 13 | - Provides good fuzzy search and stemming capabilities 14 | - Is fast enough to be able to show results while typing 15 | - Does not consume too much CPU and RAM on the client-side, especially for the statically built version 16 | 17 | ## Considered Options 18 | 19 | - Option 1: Fuse.js 20 | - Option 2: Lunr.js 21 | 22 | ## Decision Outcome 23 | 24 | Chosen option: "Option 2: Lunr.js". 25 | 26 | ## Pros and Cons of the Options 27 | 28 | ### Option 1: Fuse.js 29 | 30 | 31 | 32 | - Fast indexing 33 | - Slow searching 34 | - Only fuzzy search, no stemming 35 | 36 | ### Option 2: Lunr.js 37 | 38 | 39 | 40 | - Slow indexing, but supports index serialization to pre-build them 41 | - Fast searching 42 | - Stemming, multi-language support 43 | - Retrieves the position of the matched tokens 44 | 45 | ## Links 46 | 47 | - 48 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrStatus.ts: -------------------------------------------------------------------------------- 1 | import { Log4brainsError, ValueObject } from "@src/domain"; 2 | 3 | type Props = { 4 | name: string; 5 | }; 6 | 7 | export class AdrStatus extends ValueObject { 8 | static DRAFT = new AdrStatus("draft"); 9 | 10 | static PROPOSED = new AdrStatus("proposed"); 11 | 12 | static REJECTED = new AdrStatus("rejected"); 13 | 14 | static ACCEPTED = new AdrStatus("accepted"); 15 | 16 | static DEPRECATED = new AdrStatus("deprecated"); 17 | 18 | static SUPERSEDED = new AdrStatus("superseded"); 19 | 20 | private constructor(name: string) { 21 | super({ name }); 22 | } 23 | 24 | get name(): string { 25 | return this.props.name; 26 | } 27 | 28 | static createFromName(name: string): AdrStatus { 29 | if (name.toLowerCase().startsWith("superseded by")) { 30 | return this.SUPERSEDED; 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 34 | const status = Object.values(AdrStatus) 35 | .filter((prop) => { 36 | return prop instanceof AdrStatus && prop.name === name.toLowerCase(); 37 | }) 38 | .pop(); 39 | if (!status) { 40 | throw new Log4brainsError("Unknown ADR status", name); 41 | } 42 | 43 | return status as AdrStatus; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrTemplate.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot, Log4brainsError } from "@src/domain"; 2 | import { Adr } from "./Adr"; 3 | import { AdrSlug } from "./AdrSlug"; 4 | import { MarkdownBody } from "./MarkdownBody"; 5 | import { PackageRef } from "./PackageRef"; 6 | 7 | type Props = { 8 | package?: PackageRef; 9 | body: MarkdownBody; 10 | }; 11 | 12 | export class AdrTemplate extends AggregateRoot { 13 | get package(): PackageRef | undefined { 14 | return this.props.package; 15 | } 16 | 17 | get body(): MarkdownBody { 18 | return this.props.body; 19 | } 20 | 21 | createAdrFromMe(slug: AdrSlug, title: string): Adr { 22 | const packageRef = slug.packagePart 23 | ? new PackageRef(slug.packagePart) 24 | : undefined; 25 | if ( 26 | (!this.package && packageRef) || 27 | (this.package && !this.package.equals(packageRef)) 28 | ) { 29 | throw new Log4brainsError( 30 | "The given slug does not match this template package name", 31 | `slug: ${slug.value} / template package: ${this.package?.name}` 32 | ); 33 | } 34 | const adr = new Adr({ 35 | slug, 36 | package: packageRef, 37 | body: this.body.clone() 38 | }); 39 | adr.setTitle(title); 40 | return adr; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/cli/src/commands/ListCommand.ts: -------------------------------------------------------------------------------- 1 | import { Log4brains, SearchAdrsFilters, AdrDtoStatus } from "@log4brains/core"; 2 | import type { AppConsole } from "@log4brains/cli-common"; 3 | 4 | type Deps = { 5 | l4bInstance: Log4brains; 6 | appConsole: AppConsole; 7 | }; 8 | 9 | export type ListCommandOpts = { 10 | statuses: string; 11 | raw: boolean; 12 | }; 13 | 14 | export class ListCommand { 15 | private readonly l4bInstance: Log4brains; 16 | 17 | private readonly console: AppConsole; 18 | 19 | constructor({ l4bInstance, appConsole }: Deps) { 20 | this.l4bInstance = l4bInstance; 21 | this.console = appConsole; 22 | } 23 | 24 | async execute(opts: ListCommandOpts): Promise { 25 | const filters: SearchAdrsFilters = {}; 26 | if (opts.statuses) { 27 | filters.statuses = opts.statuses.split(",") as AdrDtoStatus[]; 28 | } 29 | const adrs = await this.l4bInstance.searchAdrs(filters); 30 | const table = this.console.createTable({ 31 | head: ["Slug", "Status", "Package", "Title"] 32 | }); 33 | adrs.forEach((adr) => { 34 | table.push([ 35 | adr.slug, 36 | adr.status.toUpperCase(), 37 | adr.package || "", 38 | adr.title || "Untitled" 39 | ]); 40 | }); 41 | this.console.printTable(table, opts.raw); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/command-handlers/CreateAdrFromTemplateCommandHandler.ts: -------------------------------------------------------------------------------- 1 | import { PackageRef } from "@src/adr/domain"; 2 | import { CommandHandler } from "@src/application"; 3 | import { CreateAdrFromTemplateCommand } from "../commands"; 4 | import { AdrRepository, AdrTemplateRepository } from "../repositories"; 5 | 6 | type Deps = { 7 | adrRepository: AdrRepository; 8 | adrTemplateRepository: AdrTemplateRepository; 9 | }; 10 | 11 | export class CreateAdrFromTemplateCommandHandler implements CommandHandler { 12 | readonly commandClass = CreateAdrFromTemplateCommand; 13 | 14 | private readonly adrRepository: AdrRepository; 15 | 16 | private readonly adrTemplateRepository: AdrTemplateRepository; 17 | 18 | constructor({ adrRepository, adrTemplateRepository }: Deps) { 19 | this.adrRepository = adrRepository; 20 | this.adrTemplateRepository = adrTemplateRepository; 21 | } 22 | 23 | async execute(command: CreateAdrFromTemplateCommand): Promise { 24 | const packageRef = command.slug.packagePart 25 | ? new PackageRef(command.slug.packagePart) 26 | : undefined; 27 | const template = await this.adrTemplateRepository.find(packageRef); 28 | const adr = template.createAdrFromMe(command.slug, command.title); 29 | await this.adrRepository.save(adr); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/web/cli/utils.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { ConsoleCapturer } from "@log4brains/cli-common"; 3 | import { Log4brains } from "@log4brains/core"; 4 | import path from "path"; 5 | 6 | let l4bInstance: Log4brains; 7 | export function getL4bInstance(): Log4brains { 8 | if (!l4bInstance) { 9 | l4bInstance = Log4brains.createFromCwd(process.env.LOG4BRAINS_CWD || "."); 10 | } 11 | return l4bInstance; 12 | } 13 | 14 | export function getNextJsDir(): string { 15 | return path.resolve(path.join(__dirname, "../nextjs")); // only one level up because bundled with microbundle 16 | } 17 | 18 | /** 19 | * #NEXTJS-HACK 20 | * We want to hide the output of Next.js when we execute CLI commands. 21 | * 22 | * @param fn The code which calls Next.js methods for which we want to capture the output 23 | */ 24 | export async function execNext(fn: () => Promise): Promise { 25 | const debug = !!process.env.DEBUG; 26 | 27 | const capturer = new ConsoleCapturer(); 28 | capturer.onLog = (method, args, stream) => { 29 | if (stream === "stderr" || debug) { 30 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 31 | capturer.doPrintln(...["[Next] ", ...args].map((a) => chalk.dim(a))); 32 | } 33 | }; 34 | 35 | capturer.start(); 36 | await fn(); 37 | capturer.stop(); 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/reusable-e2e-tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | npm-package-fullname: 5 | required: true 6 | type: string 7 | experimental: 8 | required: false 9 | type: boolean 10 | default: false 11 | os: 12 | required: true 13 | type: string 14 | # Set only one of these two inputs: 15 | node-version: 16 | required: false 17 | type: string 18 | node-version-file: 19 | required: false 20 | type: string 21 | 22 | defaults: 23 | run: 24 | shell: bash 25 | 26 | jobs: 27 | e2e-tests: 28 | runs-on: ${{ inputs.os }} 29 | continue-on-error: ${{ inputs.experimental }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 # required by Log4brains to work correctly (needs the whole Git history) 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version-file: ${{ inputs.node-version-file }} 37 | node-version: ${{ inputs.node-version }} 38 | cache: yarn 39 | cache-dependency-path: yarn.lock 40 | 41 | # But we do install log4brains from the registry 42 | - run: npm install -g ${{ inputs.npm-package-fullname }} 43 | 44 | - name: E2E Tests on the registry version 45 | run: yarn e2e 46 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/MarkdownToc/MarkdownToc.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Meta } from "@storybook/react"; 3 | import { compiler as mdCompiler } from "markdown-to-jsx"; 4 | import { MarkdownHeading } from "../MarkdownHeading"; 5 | import { MarkdownToc } from "./MarkdownToc"; 6 | 7 | const markdown = `# Header 1 8 | Lorem Ipsum 9 | 10 | ## Header 1.1 11 | 12 | ### Header 1.1.1 13 | 14 | ### Header 1.1.2 15 | 16 | ## Header 1.2 17 | 18 | #### Subtitle without direct parent 19 | 20 | test 21 | 22 | # Header 2 23 | 24 | hello`; 25 | 26 | const options = { 27 | overrides: { 28 | h1: { 29 | component: MarkdownHeading, 30 | props: { variant: "h1" } 31 | }, 32 | h2: { 33 | component: MarkdownHeading, 34 | props: { variant: "h2" } 35 | }, 36 | h3: { 37 | component: MarkdownHeading, 38 | props: { variant: "h3" } 39 | }, 40 | h4: { 41 | component: MarkdownHeading, 42 | props: { variant: "h4" } 43 | } 44 | } 45 | }; 46 | 47 | const content = mdCompiler(markdown, options) as React.ReactElement<{ 48 | children: React.ReactElement; 49 | }>; 50 | 51 | export default { 52 | title: "MarkdownToc", 53 | component: MarkdownToc 54 | } as Meta; 55 | 56 | export function Default() { 57 | return ; 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/scheduled-weekly-e2e-stable.yml: -------------------------------------------------------------------------------- 1 | name: Weekly E2E tests (stable only) 2 | on: 3 | workflow_dispatch: 4 | 5 | # Run on every Wednesday to check possible regressions caused by dependency updates 6 | # Like this one: https://github.com/thomvaill/log4brains/issues/27 7 | schedule: 8 | - cron: "0 4 * * 3" 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | load-nodejs-supported-versions: 16 | uses: ./.github/workflows/reusable-load-nodejs-supported-versions.yml 17 | 18 | e2e: 19 | needs: load-nodejs-supported-versions 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest, windows-latest, macos-latest] 23 | node-version: ${{ fromJson(needs.load-nodejs-supported-versions.outputs.node_versions) }} 24 | uses: ./.github/workflows/reusable-e2e-tests.yml 25 | with: 26 | npm-package-fullname: log4brains # stable version 27 | os: ${{ matrix.os }} 28 | node-version: ${{ matrix.node-version }} 29 | 30 | e2e-tests-nodejs-current: 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest, windows-latest, macos-latest] 34 | uses: ./.github/workflows/reusable-e2e-tests.yml 35 | with: 36 | npm-package-fullname: log4brains # stable version 37 | os: ${{ matrix.os }} 38 | node-version: current 39 | experimental: true # best effort mode 40 | -------------------------------------------------------------------------------- /packages/global-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { AppConsole, FailureExit } from "@log4brains/cli-common"; 3 | import { Log4brainsConfigNotFoundError } from "@log4brains/core"; 4 | import { createGlobalCli } from "./cli"; 5 | 6 | const debug = !!process.env.DEBUG; 7 | const dev = process.env.NODE_ENV === "development"; 8 | const appConsole = new AppConsole({ debug, traces: debug || dev }); 9 | 10 | function handleError(err: unknown): void { 11 | if (appConsole.isSpinning()) { 12 | appConsole.stopSpinner(true); 13 | } 14 | 15 | if (err instanceof FailureExit) { 16 | process.exit(1); 17 | } 18 | 19 | if (err instanceof Log4brainsConfigNotFoundError) { 20 | appConsole.fatal(`Cannot find ${chalk.bold(".log4brains.yml")}`); 21 | appConsole.printlnErr( 22 | chalk.red( 23 | `You are in the wrong directory or you need to run ${chalk.cyan( 24 | "log4brains init" 25 | )}` 26 | ) 27 | ); 28 | process.exit(1); 29 | } 30 | 31 | appConsole.fatal(err as Error); 32 | process.exit(1); 33 | } 34 | 35 | try { 36 | // eslint-disable-next-line 37 | const pkgVersion = require("../package.json").version as string; 38 | 39 | const cli = createGlobalCli({ version: pkgVersion, appConsole }); 40 | cli.parseAsync(process.argv).catch(handleError); 41 | } catch (e) { 42 | handleError(e); 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrFile.ts: -------------------------------------------------------------------------------- 1 | import { Log4brainsError, ValueObject } from "@src/domain"; 2 | import type { AdrSlug } from "./AdrSlug"; 3 | import { FilesystemPath } from "./FilesystemPath"; 4 | 5 | type Props = { 6 | path: FilesystemPath; 7 | }; 8 | 9 | const reservedFilenames = [ 10 | "template.md", 11 | "readme.md", 12 | "index.md", 13 | "backlog.md" 14 | ]; 15 | 16 | export class AdrFile extends ValueObject { 17 | constructor(path: FilesystemPath) { 18 | super({ path }); 19 | 20 | if (path.extension.toLowerCase() !== ".md") { 21 | throw new Log4brainsError( 22 | "Only .md files are supported", 23 | path.pathRelativeToCwd 24 | ); 25 | } 26 | 27 | if (reservedFilenames.includes(path.basename.toLowerCase())) { 28 | throw new Log4brainsError("Reserved ADR filename", path.basename); 29 | } 30 | } 31 | 32 | get path(): FilesystemPath { 33 | return this.props.path; 34 | } 35 | 36 | static isPathValid(path: FilesystemPath): boolean { 37 | try { 38 | // eslint-disable-next-line no-new 39 | new AdrFile(path); 40 | return true; 41 | } catch (e) { 42 | return false; 43 | } 44 | } 45 | 46 | static createFromSlugInFolder( 47 | folder: FilesystemPath, 48 | slug: AdrSlug 49 | ): AdrFile { 50 | return new AdrFile(folder.join(`${slug.namePart}.md`)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/MarkdownToc/MarkdownToc.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { compiler as mdCompiler } from "markdown-to-jsx"; 3 | import TestRenderer from "react-test-renderer"; 4 | import { MarkdownHeading } from "../MarkdownHeading"; 5 | import { MarkdownToc } from "./MarkdownToc"; 6 | 7 | const markdown = `# Header 1 8 | Lorem Ipsum 9 | 10 | ## Header 1.1 11 | 12 | ### Header 1.1.1 13 | 14 | ### Header 1.1.2 15 | 16 | ## Header 1.2 17 | 18 | #### Subtitle without direct parent 19 | 20 | test 21 | 22 | # Header 2 23 | 24 | hello`; 25 | 26 | const options = { 27 | overrides: { 28 | h1: { 29 | component: MarkdownHeading, 30 | props: { variant: "h1" } 31 | }, 32 | h2: { 33 | component: MarkdownHeading, 34 | props: { variant: "h2" } 35 | }, 36 | h3: { 37 | component: MarkdownHeading, 38 | props: { variant: "h3" } 39 | }, 40 | h4: { 41 | component: MarkdownHeading, 42 | props: { variant: "h4" } 43 | } 44 | } 45 | }; 46 | 47 | describe("Toc", () => { 48 | const content = mdCompiler(markdown, options) as React.ReactElement<{ 49 | children: React.ReactElement; 50 | }>; 51 | 52 | it("renders correctly", () => { 53 | const tree = TestRenderer.create( 54 | 55 | ); 56 | expect(tree.toJSON()).toMatchSnapshot(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /docs/adr/20201016-use-the-adr-slug-as-its-unique-id.md: -------------------------------------------------------------------------------- 1 | # Use the ADR slug as its unique ID 2 | 3 | - Status: accepted 4 | - Date: 2020-10-16 5 | 6 | ## Context and Problem Statement 7 | 8 | Currently, ADR files follow this format: `NNNN-adr-title.md`, with NNNN being an incremental number from `0000` to `9999`. 9 | It causes an issue during a `git merge` when two developers have created a new ADR on their respective branch. 10 | There is a conflict because [an ADR number must be unique](20200926-use-the-adr-number-as-its-unique-id.md). 11 | 12 | ## Decision 13 | 14 | From now on, we won't use ADR numbers anymore. 15 | 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. 16 | 17 | As a result, there won't have conflicts anymore and the files will still be correctly sorted in the IDE thanks to the date. 18 | 19 | Finally, the ADRs will be sorted with these rules (ordered by priority): 20 | 21 | 1. By Date field, in the markdown file (if present) 22 | 2. By Git creation date (does not follow renames) 23 | 3. By file creation date if no versioned yet 24 | 4. By slug 25 | 26 | The core library is responsible for sorting. 27 | 28 | ## Links 29 | 30 | - Supersedes [20200926-use-the-adr-number-as-its-unique-id](20200926-use-the-adr-number-as-its-unique-id.md) 31 | -------------------------------------------------------------------------------- /scripts/wait-for-npm-version-to-be-published.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | package_name="${1:-}" 5 | package_version="${2:-}" 6 | wait_minutes="${3:-}" 7 | 8 | usage() { 9 | echo "Usage: $0 " 10 | } 11 | 12 | if [[ -z "${package_name}" ]] 13 | then 14 | echo "Missing package_name" 15 | usage 16 | exit 1 17 | fi 18 | 19 | if [[ -z "${package_version}" ]] 20 | then 21 | echo "Missing package_version" 22 | usage 23 | exit 1 24 | fi 25 | 26 | if [[ -z "${wait_minutes}" ]] || ! [[ "${wait_minutes}" =~ ^-?[0-9]+$ ]] 27 | then 28 | echo "Missing wait_minutes or is not a valid integer" 29 | usage 30 | exit 1 31 | fi 32 | 33 | echo "Checking if ${package_name}@${package_version} is available on NPM (waiting max ${wait_minutes} min)..." 34 | 35 | i=0 36 | while ! npm view "${package_name}" versions --json | jq -e --arg package_version "${package_version}" 'index($package_version)' &> /dev/null 37 | do 38 | if [[ ${i} -gt ${wait_minutes} ]] 39 | then 40 | echo "Failure for more than ${wait_minutes} minutes" 41 | echo "Available versions:" 42 | npm view "${package_name}" versions --json 43 | echo "Abort" 44 | exit 1 45 | fi 46 | 47 | echo "${package_name}@${package_version} is not available yet on NPM. Let's wait 60s..." 48 | sleep 60 49 | ((i=i+1)) 50 | done 51 | 52 | echo "${package_name}@${package_version} is available" 53 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/lib/toc-utils/TocBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Toc } from "./Toc"; 2 | import { TocContainer } from "./TocContainer"; 3 | 4 | export class TocBuilder { 5 | private readonly root: Toc; 6 | 7 | private current: TocContainer; 8 | 9 | constructor() { 10 | this.root = new Toc(); 11 | this.current = this.root; 12 | } 13 | 14 | addSection(level: number, title: string, id: string): void { 15 | if (level <= 0) { 16 | throw new Error("Level must be > 0"); 17 | } 18 | 19 | if (level < this.current.getLevel() + 1) { 20 | // eg: section to add = H2, current section = H2 -> we have to step back from one level 21 | if (!this.current.parent) { 22 | throw new Error("Never happens thanks to recursion"); 23 | } 24 | this.current = this.current.parent; 25 | this.addSection(level, title, id); 26 | } else if (level > this.current.getLevel() + 1) { 27 | // eg: section to add = H4, current section = H2 -> we have to create an empty intermediate section 28 | this.current = this.current.createChild("", ""); 29 | this.addSection(level, title, id); 30 | } else if (level === this.current.getLevel() + 1) { 31 | // recursion stop condition 32 | // eg: section to add = H2, current section = H1 33 | this.current = this.current.createChild(title, id); 34 | } 35 | } 36 | 37 | getToc(): Toc { 38 | return this.root; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@log4brains/cli", 3 | "version": "1.1.0", 4 | "description": "Log4brains architecture knowledge base CLI", 5 | "keywords": [ 6 | "log4brains" 7 | ], 8 | "author": "Thomas Vaillant ", 9 | "license": "Apache-2.0", 10 | "private": false, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "homepage": "https://github.com/thomvaill/log4brains", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/thomvaill/log4brains", 18 | "directory": "packages/cli" 19 | }, 20 | "engines": { 21 | "node": ">=18" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "source": "./src/index.ts", 27 | "main": "./dist/index.js", 28 | "module": "./dist/index.module.js", 29 | "types": "./dist/index.d.ts", 30 | "scripts": { 31 | "dev": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node watch", 32 | "build": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node", 33 | "clean": "rimraf ./dist", 34 | "typescript": "tsc --noEmit", 35 | "lint": "eslint . --max-warnings=0", 36 | "prepublishOnly": "yarn build" 37 | }, 38 | "dependencies": { 39 | "@log4brains/cli-common": "1.1.0", 40 | "@log4brains/core": "1.1.0", 41 | "commander": "^6.1.0", 42 | "execa": "^5.0.0", 43 | "terminal-link": "^2.1.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/Markdown/components/AdrLink/AdrLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AdrDtoStatus } from "@log4brains/core"; 3 | import { makeStyles, createStyles } from "@material-ui/core/styles"; 4 | import { Link as MuiLink } from "@material-ui/core"; 5 | import Link from "next/link"; 6 | import clsx from "clsx"; 7 | 8 | const useStyles = makeStyles(() => 9 | createStyles({ 10 | // TODO: refactor with AdrMenu.tsx 11 | draftLink: {}, 12 | proposedLink: {}, 13 | acceptedLink: {}, 14 | rejectedLink: { 15 | textDecoration: "line-through" 16 | }, 17 | deprecatedLink: { 18 | textDecoration: "line-through" 19 | }, 20 | supersededLink: { 21 | textDecoration: "line-through" 22 | } 23 | }) 24 | ); 25 | 26 | type AdrLinkProps = { 27 | slug: string; 28 | status: AdrDtoStatus; 29 | // eslint-disable-next-line react/no-unused-prop-types 30 | package?: string; 31 | title?: string; 32 | customLabel?: string; 33 | }; 34 | 35 | export function AdrLink({ slug, status, title, customLabel }: AdrLinkProps) { 36 | const classes = useStyles(); 37 | 38 | return ( 39 | 40 | 45 | {customLabel || title || "Untitled"} 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrRelation.test.ts: -------------------------------------------------------------------------------- 1 | import { Adr } from "./Adr"; 2 | import { AdrFile } from "./AdrFile"; 3 | import { AdrRelation } from "./AdrRelation"; 4 | import { AdrSlug } from "./AdrSlug"; 5 | import { FilesystemPath } from "./FilesystemPath"; 6 | import { MarkdownBody } from "./MarkdownBody"; 7 | import { PackageRef } from "./PackageRef"; 8 | 9 | describe("AdrRelation", () => { 10 | beforeAll(() => { 11 | Adr.setTz("Etc/UTC"); 12 | }); 13 | afterAll(() => { 14 | Adr.clearTz(); 15 | }); 16 | 17 | it("correctly prints to markdown", () => { 18 | const from = new Adr({ 19 | slug: new AdrSlug("from"), 20 | file: new AdrFile(new FilesystemPath("/", "docs/adr/from.md")), 21 | body: new MarkdownBody("") 22 | }); 23 | const to = new Adr({ 24 | slug: new AdrSlug("test/to"), 25 | package: new PackageRef("test"), 26 | file: new AdrFile( 27 | new FilesystemPath("/", "packages/test/docs/adr/to.md") 28 | ), 29 | body: new MarkdownBody("") 30 | }); 31 | 32 | const relation1 = new AdrRelation(from, "superseded by", to); 33 | expect(relation1.toMarkdown()).toEqual( 34 | "superseded by [test/to](../../packages/test/docs/adr/to.md)" 35 | ); 36 | 37 | const relation2 = new AdrRelation(from, "refines", to); 38 | expect(relation2.toMarkdown()).toEqual( 39 | "refines [test/to](../../packages/test/docs/adr/to.md)" 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/web/docs/adr/20200927-avoid-react-fc-type.md: -------------------------------------------------------------------------------- 1 | # Avoid React.FC type 2 | 3 | - Status: accepted 4 | - Date: 2020-09-27 5 | - Source: 6 | 7 | ## Context 8 | 9 | Facebook has removed `React.FC` from their base template for a Typescript 10 | project. The reason for this was that it was found to be an unnecessary feature 11 | with next to no benefits in combination with a few downsides. 12 | 13 | The main reasons were: 14 | 15 | - **children props** were implicitly added 16 | - **Generic Type** was not supported on children 17 | 18 | Read more about the removal in 19 | [this PR](https://github.com/facebook/create-react-app/pull/8177). 20 | 21 | ## Decision 22 | 23 | To keep our codebase up to date, we have decided that `React.FC` and `React.SFC` 24 | should be avoided in our codebase when adding new code. 25 | 26 | Here is an example: 27 | 28 | ```typescript 29 | /* Avoid this: */ 30 | type BadProps = { text: string }; 31 | const BadComponent: FC = ({ text, children }) => ( 32 |
33 |
{text}
34 | {children} 35 |
36 | ); 37 | 38 | /* Do this instead: */ 39 | type GoodProps = { text: string; children?: React.ReactNode }; 40 | const GoodComponent = ({ text, children }: GoodProps) => ( 41 |
42 |
{text}
43 | {children} 44 |
45 | ); 46 | ``` 47 | -------------------------------------------------------------------------------- /packages/cli-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@log4brains/cli-common", 3 | "version": "1.1.0", 4 | "description": "Log4brains architecture knowledge base common CLI features", 5 | "keywords": [ 6 | "log4brains" 7 | ], 8 | "author": "Thomas Vaillant ", 9 | "license": "Apache-2.0", 10 | "private": false, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "homepage": "https://github.com/thomvaill/log4brains", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/thomvaill/log4brains", 18 | "directory": "packages/cli-common" 19 | }, 20 | "engines": { 21 | "node": ">=18" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "source": "./src/index.ts", 27 | "main": "./dist/index.js", 28 | "module": "./dist/index.module.js", 29 | "types": "./dist/index.d.ts", 30 | "scripts": { 31 | "dev": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node watch", 32 | "dev:test": "nodemon", 33 | "build": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node", 34 | "clean": "rimraf ./dist", 35 | "typescript": "tsc --noEmit", 36 | "lint": "eslint . --max-warnings=0", 37 | "prepublishOnly": "yarn build" 38 | }, 39 | "devDependencies": { 40 | "@types/inquirer": "^7.3.1" 41 | }, 42 | "dependencies": { 43 | "chalk": "^4.1.0", 44 | "cli-table3": "^0.6.0", 45 | "inquirer": "^7.3.3", 46 | "ora": "^5.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/SearchBox/SearchBox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Meta, Story } from "@storybook/react"; 3 | import { AppBar, Toolbar } from "@material-ui/core"; 4 | import { SearchBox, SearchBoxProps } from "./SearchBox"; 5 | import { adrMocks } from "../../../.storybook/mocks"; 6 | 7 | const Template: Story = (args) => ; 8 | 9 | export default { 10 | title: "SearchBox", 11 | component: SearchBox, 12 | decorators: [ 13 | (DecoratedStory) => ( 14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 | ) 22 | ] 23 | } as Meta; 24 | 25 | export const Closed = Template.bind({}); 26 | Closed.args = {}; 27 | 28 | export const Open = Template.bind({}); 29 | Open.args = { 30 | open: true 31 | }; 32 | 33 | export const OpenWithResults = Template.bind({}); 34 | OpenWithResults.args = { 35 | open: true, 36 | query: "Test", 37 | results: adrMocks.map((adr) => ({ 38 | title: adr.title, 39 | href: `/adr/${adr.slug}` 40 | })) 41 | }; 42 | 43 | export const OpenLoading = Template.bind({}); 44 | OpenLoading.args = { 45 | open: true, 46 | query: "test", 47 | results: [], 48 | loading: true 49 | }; 50 | 51 | export const OpenWithoutResults = Template.bind({}); 52 | OpenWithoutResults.args = { 53 | open: true, 54 | query: "cdlifsdilhfsd", 55 | results: [] 56 | }; 57 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/api/types/AdrDto.ts: -------------------------------------------------------------------------------- 1 | import { GitProvider } from "@src/infrastructure/config"; 2 | 3 | export type AdrDtoStatus = 4 | | "draft" 5 | | "proposed" 6 | | "rejected" 7 | | "accepted" 8 | | "deprecated" 9 | | "superseded"; 10 | 11 | // Dates are string (Date.toJSON()) because because Next.js cannot serialize Date objects 12 | 13 | export type AdrDto = Readonly<{ 14 | slug: string; // Follows this pattern: / or just when the ADR does not belong to a specific package 15 | package: string | null; // Null when the ADR does not belong to a package 16 | title: string | null; 17 | status: AdrDtoStatus; 18 | supersededBy: string | null; // Optionally contains the target ADR slug when status === "superseded" 19 | tags: string[]; // Can be empty 20 | deciders: string[]; // Can be empty. In this case, it is encouraged to use lastEditAuthor as the only decider 21 | body: Readonly<{ 22 | rawMarkdown: string; 23 | enhancedMdx: string; 24 | }>; 25 | creationDate: string; // Comes from Git or filesystem 26 | lastEditDate: string; // Comes from Git or filesystem 27 | lastEditAuthor: string; // Comes from Git (Git last author, or current Git user.name when unversioned, or "Anonymous" when Git is not installed) 28 | publicationDate: string | null; // Comes from the Markdown body 29 | file: Readonly<{ 30 | relativePath: string; 31 | absolutePath: string; 32 | }>; 33 | repository?: Readonly<{ 34 | provider: GitProvider; 35 | viewUrl: string; 36 | }>; 37 | }>; 38 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/pages/adr/[...slug].tsx: -------------------------------------------------------------------------------- 1 | import { GetStaticProps, GetStaticPaths } from "next"; 2 | import { getLog4brainsInstance } from "../../lib/core-api"; 3 | import { getConfig } from "../../lib/next"; 4 | import { AdrScene, AdrSceneProps } from "../../scenes"; 5 | import { toAdr } from "../../lib-shared/types"; 6 | 7 | export default AdrScene; 8 | 9 | export const getStaticPaths: GetStaticPaths = async () => { 10 | const adrs = await getLog4brainsInstance().searchAdrs(); 11 | const paths = adrs.map((adr) => { 12 | return { params: { slug: adr.slug.split("/") } }; 13 | }); 14 | return { 15 | paths, 16 | fallback: 17 | process.env.LOG4BRAINS_PHASE === "initial-build" ? "blocking" : false 18 | }; 19 | }; 20 | 21 | export const getStaticProps: GetStaticProps = async ({ 22 | params 23 | }) => { 24 | const l4bInstance = getLog4brainsInstance(); 25 | 26 | if (params === undefined || !params.slug) { 27 | return { notFound: true }; 28 | } 29 | 30 | const currentSlug = (params.slug as string[]).join("/"); 31 | const currentAdr = await l4bInstance.getAdrBySlug(currentSlug); 32 | if (!currentAdr) { 33 | return { notFound: true }; 34 | } 35 | 36 | return { 37 | props: { 38 | projectName: l4bInstance.config.project.name, 39 | currentAdr: toAdr( 40 | currentAdr, 41 | currentAdr.supersededBy 42 | ? await l4bInstance.getAdrBySlug(currentAdr.supersededBy) 43 | : undefined 44 | ), 45 | l4bVersion: getConfig().serverRuntimeConfig.VERSION 46 | }, 47 | revalidate: 1 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/web/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import commander from "commander"; 2 | import type { AppConsole } from "@log4brains/cli-common"; 3 | import { previewCommand, buildCommand } from "./commands"; 4 | 5 | type StartEditorCommandOpts = { 6 | port: string; 7 | open: boolean; 8 | }; 9 | type BuildCommandOpts = { 10 | out: string; 11 | basePath: string; 12 | }; 13 | 14 | type Deps = { 15 | appConsole: AppConsole; 16 | }; 17 | 18 | export function createWebCli({ appConsole }: Deps): commander.Command { 19 | const program = new commander.Command(); 20 | 21 | program 22 | .command("preview [adr]") 23 | .description("Start Log4brains locally to preview your changes", { 24 | adr: 25 | "If provided, will automatically open your browser to this specific ADR" 26 | }) 27 | .option("-p, --port ", "Port to listen on", "4004") 28 | .option("--no-open", "Do not open the browser automatically", false) 29 | .action( 30 | (adr: string, opts: StartEditorCommandOpts): Promise => { 31 | return previewCommand( 32 | { appConsole }, 33 | parseInt(opts.port, 10), 34 | opts.open, 35 | adr 36 | ); 37 | } 38 | ); 39 | 40 | program 41 | .command("build") 42 | .description("Build Log4brains as a deployable static website") 43 | .option("-o, --out ", "Output path", ".log4brains/out") 44 | .option("--basePath ", "Custom base path", "") 45 | .action( 46 | (opts: BuildCommandOpts): Promise => { 47 | return buildCommand({ appConsole }, opts.out, opts.basePath); 48 | } 49 | ); 50 | 51 | return program; 52 | } 53 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC1091 3 | set -euo pipefail 4 | 5 | SELF_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | ROOT_PATH="$(cd "${SELF_PATH}/.." && pwd)" 7 | readonly SELF_PATH ROOT_PATH 8 | 9 | if [[ ! -f "${SELF_PATH}/release-credentials.local.sh" ]] 10 | then 11 | echo "Please create the release-credentials.local.sh file with the following exports: NODE_AUTH_TOKEN, GH_TOKEN" 12 | exit 1 13 | fi 14 | source "${SELF_PATH}/release-credentials.local.sh" 15 | npm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" # writes to ~/.npmrc 16 | npm config set registry https://registry.npmjs.org/ 17 | 18 | cd "${ROOT_PATH}" 19 | 20 | if [[ "$(git rev-parse --abbrev-ref HEAD)" != "develop" ]] 21 | then 22 | echo "Please run this command from the develop branch" 23 | exit 1 24 | fi 25 | 26 | git pull 27 | 28 | echo "" 29 | echo "Release checklist:" 30 | echo " - the last build pipeline is successful: https://github.com/thomvaill/log4brains/actions/workflows/build.yml" 31 | echo " - the example is working: https://thomvaill.github.io/log4brains/adr/" 32 | echo " - manual smoketests are OK (log4brains adr new, log4brains adr list, log4brains preview)" 33 | read -rp "Press any key to continue or Ctrl+C to abort..." 34 | 35 | yarn lerna publish \ 36 | --conventional-commits \ 37 | --conventional-graduate \ 38 | --exact \ 39 | --create-release github 40 | 41 | echo "" 42 | echo "Done!" 43 | echo "Please now monitor the Post-Release actions: https://github.com/thomvaill/log4brains/actions" 44 | echo "And don't forget to edit CHANGELOG.md and the Github release to write a clean changelog of this release" 45 | -------------------------------------------------------------------------------- /docs/adr/20200925-use-prettier-eslint-airbnb-for-the-code-style.md: -------------------------------------------------------------------------------- 1 | # Use Prettier-ESLint Airbnb for the code style 2 | 3 | - Status: accepted 4 | - Date: 2020-09-25 5 | 6 | ## Context and Problem Statement 7 | 8 | We have to choose our lint and format tools, and the code style to enforce as well. 9 | 10 | ## Considered Options 11 | 12 | - Prettier only 13 | - ESLint only 14 | - ESLint with Airbnb code style 15 | - ESLint with StandardJS code style 16 | - ESLint with Google code style 17 | - Prettier-ESLint with Airbnb code style 18 | - Prettier-ESLint with StandardJS code style 19 | - Prettier-ESLint with Google code style 20 | 21 | ## Decision Outcome 22 | 23 | Chosen option: "Prettier-ESLint with Airbnb code style", because 24 | 25 | - 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)) 26 | - Prettier-ESLint enforce some additional code style. We like it because the more opinionated the code style is, the less debates there will be :-) 27 | 28 | In addition, we use also Prettier to format json and markdown files. 29 | 30 | ### Positive Consequences 31 | 32 | - 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 33 | - 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 34 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/AdrStatusChip/AdrStatusChip.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chip } from "@material-ui/core"; 3 | import { 4 | grey, 5 | indigo, 6 | deepOrange, 7 | lightGreen, 8 | brown 9 | } from "@material-ui/core/colors"; 10 | import { createStyles, Theme, makeStyles } from "@material-ui/core/styles"; 11 | import type { AdrDtoStatus } from "@log4brains/core"; 12 | import clsx from "clsx"; 13 | 14 | // Styles are inspired by the MUI "Badge" styles 15 | const useStyles = makeStyles((theme: Theme) => 16 | createStyles({ 17 | root: { 18 | fontSize: "0.74rem", 19 | fontWeight: theme.typography.fontWeightMedium, 20 | height: "18px", 21 | verticalAlign: "text-bottom" 22 | }, 23 | label: { 24 | padding: "0 6px" 25 | }, 26 | draft: { 27 | color: grey[800] 28 | }, 29 | proposed: { 30 | color: indigo[800] 31 | }, 32 | rejected: { 33 | color: deepOrange[800] 34 | }, 35 | accepted: { 36 | color: lightGreen[800] 37 | }, 38 | deprecated: { 39 | color: brown[600] 40 | }, 41 | superseded: { 42 | color: brown[600] 43 | } 44 | }) 45 | ); 46 | 47 | export type AdrStatusChipProps = { 48 | className?: string; 49 | status: AdrDtoStatus; 50 | }; 51 | 52 | export function AdrStatusChip({ className, status }: AdrStatusChipProps) { 53 | const classes = useStyles(); 54 | return ( 55 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /packages/core/src/adr/application/query-handlers/SearchAdrsQueryHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { mock, mockClear } from "jest-mock-extended"; 2 | import { AdrRepository } from "@src/adr/application"; 3 | import { 4 | Adr, 5 | AdrFile, 6 | AdrSlug, 7 | AdrStatus, 8 | FilesystemPath, 9 | MarkdownBody 10 | } from "@src/adr/domain"; 11 | import { SearchAdrsQuery } from "../queries"; 12 | import { SearchAdrsQueryHandler } from "./SearchAdrsQueryHandler"; 13 | 14 | describe("SearchAdrsQueryHandler", () => { 15 | const adr1 = new Adr({ 16 | slug: new AdrSlug("adr1"), 17 | file: new AdrFile(new FilesystemPath("/", "adr1.md")), 18 | body: new MarkdownBody("") 19 | }); 20 | const adr2 = new Adr({ 21 | slug: new AdrSlug("adr2"), 22 | file: new AdrFile(new FilesystemPath("/", "adr2.md")), 23 | body: new MarkdownBody("") 24 | }); 25 | 26 | const adrRepository = mock(); 27 | adrRepository.findAll.mockReturnValue(Promise.resolve([adr1, adr2])); 28 | 29 | const handler = new SearchAdrsQueryHandler({ adrRepository }); 30 | 31 | beforeAll(() => { 32 | Adr.setTz("Etc/UTC"); 33 | }); 34 | afterAll(() => { 35 | Adr.clearTz(); 36 | }); 37 | 38 | beforeEach(() => { 39 | mockClear(adrRepository); 40 | }); 41 | 42 | it("returns all ADRs when no filter", async () => { 43 | const adrs = await handler.execute(new SearchAdrsQuery({})); 44 | expect(adrs).toHaveLength(2); 45 | }); 46 | 47 | it("filters the ADRs on their status", async () => { 48 | const adrs = await handler.execute( 49 | new SearchAdrsQuery({ statuses: [AdrStatus.createFromName("proposed")] }) 50 | ); 51 | expect(adrs).toHaveLength(0); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrSlug.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import slugify from "slugify"; 3 | import { Log4brainsError, ValueObject } from "@src/domain"; 4 | import { AdrFile } from "./AdrFile"; 5 | import { PackageRef } from "./PackageRef"; 6 | 7 | type Props = { 8 | value: string; 9 | }; 10 | 11 | export class AdrSlug extends ValueObject { 12 | constructor(value: string) { 13 | super({ value }); 14 | 15 | if (this.namePart.includes("/")) { 16 | throw new Log4brainsError( 17 | "The / character is not allowed in the name part of an ADR slug", 18 | value 19 | ); 20 | } 21 | } 22 | 23 | get value(): string { 24 | return this.props.value; 25 | } 26 | 27 | get packagePart(): string | undefined { 28 | const s = this.value.split("/", 2); 29 | return s.length >= 2 ? s[0] : undefined; 30 | } 31 | 32 | get namePart(): string { 33 | const s = this.value.split("/", 2); 34 | return s.length >= 2 ? s[1] : s[0]; 35 | } 36 | 37 | static createFromFile(file: AdrFile, packageRef?: PackageRef): AdrSlug { 38 | const localSlug = file.path.basenameWithoutExtension; 39 | return new AdrSlug( 40 | packageRef ? `${packageRef.name}/${localSlug}` : localSlug 41 | ); 42 | } 43 | 44 | static createFromTitle( 45 | title: string, 46 | packageRef?: PackageRef, 47 | date?: Date 48 | ): AdrSlug { 49 | const slugifiedTitle = slugify(title, { 50 | lower: true, 51 | strict: true 52 | }).replace(/-*$/, ""); 53 | const localSlug = `${moment(date).format("YYYYMMDD")}-${slugifiedTitle}`; 54 | return new AdrSlug( 55 | packageRef ? `${packageRef.name}/${localSlug}` : localSlug 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/init/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@log4brains/init", 3 | "version": "1.1.0", 4 | "description": "Log4brains architecture knowledge base initialization CLI", 5 | "keywords": [ 6 | "log4brains" 7 | ], 8 | "author": "Thomas Vaillant ", 9 | "license": "Apache-2.0", 10 | "private": false, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "homepage": "https://github.com/thomvaill/log4brains", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/thomvaill/log4brains", 18 | "directory": "packages/init" 19 | }, 20 | "engines": { 21 | "node": ">=18" 22 | }, 23 | "files": [ 24 | "assets", 25 | "dist" 26 | ], 27 | "source": "./src/index.ts", 28 | "main": "./dist/index.js", 29 | "module": "./dist/index.module.js", 30 | "types": "./dist/index.d.ts", 31 | "scripts": { 32 | "dev": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node watch", 33 | "build": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node", 34 | "clean": "rimraf ./dist", 35 | "typescript": "tsc --noEmit", 36 | "test": "exit 0", 37 | "test-watch": "jest --watch", 38 | "lint": "eslint . --max-warnings=0", 39 | "prepublishOnly": "yarn build" 40 | }, 41 | "dependencies": { 42 | "@log4brains/cli-common": "1.1.0", 43 | "chalk": "^4.1.0", 44 | "commander": "^6.1.0", 45 | "execa": "^4.1.0", 46 | "mkdirp": "^1.0.4", 47 | "moment-timezone": "^0.5.32", 48 | "terminal-link": "^2.1.1", 49 | "yaml": "^1.10.0" 50 | }, 51 | "devDependencies": { 52 | "@types/edit-json-file": "^1.4.0", 53 | "esm": "^3.2.25", 54 | "ts-node": "^9.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/config/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | type ProjectPackageConfig = Readonly<{ 4 | name: string; 5 | path: string; 6 | adrFolder: string; 7 | }>; 8 | 9 | const projectPackageSchema = Joi.object({ 10 | name: Joi.string().hostname().required(), 11 | path: Joi.string().required(), 12 | adrFolder: Joi.string().required() 13 | }); 14 | 15 | export const gitProviders = [ 16 | "github", 17 | "gitlab", 18 | "bitbucket", 19 | "generic" 20 | ] as const; 21 | export type GitProvider = typeof gitProviders[number]; 22 | 23 | // Optional values are automatically guessed at configuration build time 24 | export type GitRepositoryConfig = Readonly<{ 25 | url?: string; 26 | provider?: GitProvider; 27 | viewFileUriPattern?: string; 28 | }>; 29 | 30 | const gitRepositorySchema = Joi.object({ 31 | url: Joi.string().uri(), // Guessed from the current Git configuration if omitted 32 | provider: Joi.string().valid(...gitProviders), // Guessed from url if omitted (useful for enterprise plans with custom domains) 33 | viewFileUriPattern: Joi.string() // Useful for unsupported providers. Example for GitHub: /blob/%branch/%path 34 | }); 35 | 36 | type ProjectConfig = Readonly<{ 37 | name: string; 38 | tz: string; 39 | adrFolder: string; 40 | packages?: ProjectPackageConfig[]; 41 | repository?: GitRepositoryConfig; 42 | }>; 43 | 44 | const projectSchema = Joi.object({ 45 | name: Joi.string().required(), 46 | tz: Joi.string().required(), 47 | adrFolder: Joi.string().required(), 48 | packages: Joi.array().items(projectPackageSchema), 49 | repository: gitRepositorySchema 50 | }); 51 | 52 | export type Log4brainsConfig = Readonly<{ 53 | project: ProjectConfig; 54 | }>; 55 | 56 | export const schema = Joi.object({ 57 | project: projectSchema.required() 58 | }); 59 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/scenes/AdrScene/AdrScene.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Meta, Story } from "@storybook/react"; 3 | import { AdrScene, AdrSceneProps } from "./AdrScene"; 4 | import { adrMocks, getMockedAdrBySlug } from "../../../.storybook/mocks"; 5 | import { Log4brainsMode, Log4brainsModeContext } from "../../contexts"; 6 | import { toAdrLight } from "../../lib-shared/types"; 7 | import { AdrBrowserLayout } from "../../layouts"; 8 | 9 | const Template: Story = (args) => ( 10 | 11 | 12 | 13 | ); 14 | 15 | export default { 16 | title: "Scenes/ADR", 17 | component: AdrScene 18 | } as Meta; 19 | 20 | export const FirstAdrWithLongTitle = Template.bind({}); 21 | FirstAdrWithLongTitle.args = { 22 | currentAdr: adrMocks[0] 23 | }; 24 | 25 | export const LastAdr = Template.bind({}); 26 | LastAdr.args = { 27 | currentAdr: adrMocks[adrMocks.length - 1] 28 | }; 29 | 30 | export const PreviewMode = Template.bind({}); 31 | PreviewMode.args = { 32 | currentAdr: adrMocks[adrMocks.length - 1] 33 | }; 34 | PreviewMode.decorators = [ 35 | (DecoratedStory) => ( 36 | 37 | 38 | 39 | ) 40 | ]; 41 | 42 | export const LotOfDeciders = Template.bind({}); 43 | LotOfDeciders.args = { 44 | currentAdr: getMockedAdrBySlug("backend/20200405-lot-of-deciders") 45 | }; 46 | 47 | export const Superseded = Template.bind({}); 48 | Superseded.args = { 49 | currentAdr: getMockedAdrBySlug("20200106-an-old-decision") 50 | }; 51 | 52 | export const Superseder = Template.bind({}); 53 | Superseder.args = { 54 | currentAdr: getMockedAdrBySlug("20200404-a-new-decision") 55 | }; 56 | -------------------------------------------------------------------------------- /packages/core/src/domain/ValueObject.test.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from "./ValueObject"; 2 | 3 | describe("ValueObject", () => { 4 | type MyVo1Props = { 5 | prop1: string; 6 | prop2: number; 7 | }; 8 | class MyVo1 extends ValueObject {} 9 | class MyVo1bis extends ValueObject {} 10 | 11 | type MyVo2Props = { 12 | prop1: string; 13 | }; 14 | class MyVo2 extends ValueObject {} 15 | 16 | describe("equals()", () => { 17 | it("returns true for the same instance", () => { 18 | const vo = new MyVo1({ prop1: "foo", prop2: 42 }); 19 | expect(vo.equals(vo)).toBeTruthy(); 20 | }); 21 | 22 | it("returns true for a different instance with the same props", () => { 23 | const vo1 = new MyVo1({ prop1: "foo", prop2: 42 }); 24 | const vo2 = new MyVo1({ prop1: "foo", prop2: 42 }); 25 | expect(vo1.equals(vo2)).toBeTruthy(); 26 | expect(vo2.equals(vo1)).toBeTruthy(); 27 | }); 28 | 29 | it("returns false for a different instance", () => { 30 | const vo1 = new MyVo1({ prop1: "foo", prop2: 42 }); 31 | const vo2 = new MyVo1({ prop1: "bar", prop2: 42 }); 32 | expect(vo1.equals(vo2)).toBeFalsy(); 33 | expect(vo2.equals(vo1)).toBeFalsy(); 34 | }); 35 | 36 | it("returns false for a different class", () => { 37 | const vo1 = new MyVo1({ prop1: "foo", prop2: 42 }); 38 | const vo2 = new MyVo2({ prop1: "foo" }); 39 | expect(vo2.equals(vo1)).toBeFalsy(); 40 | }); 41 | 42 | it("returns false for a different class with the same props", () => { 43 | const vo1 = new MyVo1({ prop1: "foo", prop2: 42 }); 44 | const vo1bis = new MyVo1bis({ prop1: "foo", prop2: 42 }); 45 | expect(vo1bis.equals(vo1)).toBeFalsy(); 46 | expect(vo1.equals(vo1bis)).toBeFalsy(); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /docs/adr/20241217-switch-back-to-github-flow.md: -------------------------------------------------------------------------------- 1 | # Switch back to GitHub Flow, but keeping the automated beta releases 2 | 3 | - Status: accepted 4 | - Date: 2024-12-17 5 | 6 | ## Context and Problem Statement 7 | 8 | The previous decision to adopt a Simplified Git Flow with a `develop` and `stable` branch aimed to provide clearer separation between development and releases while enabling automated beta releases. However, we faced tooling limitations. 9 | 10 | Specifically, **Lerna**, which we use for managing our monorepo, does not integrate well with Git Flow processes ([Lerna issue #2023](https://github.com/lerna/lerna/issues/2023#issuecomment-480402592)), when managing both a `develop` and `stable` branches: we cannot make Lerna publish a stable release from a different branch than the main one. 11 | 12 | ## Decision Drivers 13 | 14 | - Make Lerna work again for stable releases 15 | - Keep the benefits of the previous workflow: 16 | - Need for automation in releases. 17 | - Be able to merge more quickly whithout breaking the stable branch, while making it possible for beta testers to test the new features. 18 | - Easier delegation to new maintainers. 19 | - Simplified yet structured process for contributors. 20 | - Clearer separation between ongoing development and stable releases. 21 | 22 | ## Considered Options 23 | 24 | - Option 1: Patch Lerna to keep the Simplified Git Flow (as described in the link above) 25 | - Option 2: Switch back to Github Flow, but keeping the automated beta releases 26 | 27 | ## Decision Outcome 28 | 29 | Chosen option: "Option 2: Switch back to Github Flow, but keeping the automated beta releases", because it fullfils all the requirements. 30 | In fact, we can still merge quickly pull requests without releasing a stable release, while keeping the automated beta release. 31 | The stable release is a manual script executed by maintainers. 32 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/MarkdownHeading/MarkdownHeading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; 3 | import { 4 | Typography, 5 | TypographyClassKey, 6 | Link as MuiLink 7 | } from "@material-ui/core"; 8 | import clsx from "clsx"; 9 | 10 | const useStyles = makeStyles((theme: Theme) => 11 | createStyles({ 12 | root: { 13 | "&:hover": { 14 | "& $link": { 15 | visibility: "visible" 16 | } 17 | } 18 | }, 19 | link: { 20 | marginLeft: "0.3ch", 21 | color: "inherit", 22 | "&:hover": { 23 | color: theme.palette.primary.main 24 | }, 25 | visibility: "hidden" 26 | } 27 | }) 28 | ); 29 | 30 | export type MarkdownHeadingProps = { 31 | children: string; 32 | id: string; 33 | variant: "h1" | "h2" | "h3" | "h4"; 34 | className?: string; 35 | }; 36 | 37 | export function MarkdownHeading({ 38 | id, 39 | children, 40 | variant, 41 | className 42 | }: MarkdownHeadingProps) { 43 | const classes = useStyles(); 44 | 45 | let typographyVariant: TypographyClassKey; 46 | switch (variant) { 47 | case "h1": 48 | typographyVariant = "h3"; 49 | break; 50 | case "h2": 51 | typographyVariant = "h4"; 52 | break; 53 | case "h3": 54 | typographyVariant = "h5"; 55 | break; 56 | case "h4": 57 | typographyVariant = "h6"; 58 | break; 59 | default: 60 | typographyVariant = "h6"; 61 | break; 62 | } 63 | 64 | return ( 65 | 72 | {children} 73 | 74 | ¶ 75 | 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /docs/adr/20200925-multi-packages-architecture-in-a-monorepo-with-yarn-and-lerna.md: -------------------------------------------------------------------------------- 1 | # Multi-packages architecture in a monorepo with Yarn and Lerna 2 | 3 | - Status: accepted 4 | - Date: 2020-09-25 5 | 6 | ## Context and Problem Statement 7 | 8 | We have to define the initial overall architecture of the project. 9 | For now, we are sure that we want to provide these features: 10 | 11 | - Local preview web UI 12 | - Static Site Generation from the CI/CD 13 | - CLI to create a new ADR quickly 14 | 15 | In the future, we might want to provide these features: 16 | 17 | - Create/edit ADRs from the local web UI 18 | - VSCode extension to create and maybe edit an ADR from the IDE 19 | - Support ADR aggregation from multiple repositories 20 | 21 | ## Considered Options 22 | 23 | - Monolith 24 | - Multi-packages, multirepo 25 | - Multi-packages, monorepo 26 | - with NPM and scripts for links and publication 27 | - with Yarn and scripts for publication 28 | - with Yarn and Lerna 29 | 30 | ## Decision Outcome 31 | 32 | Chosen option: "Multi-packages, monorepo, with Yarn and Lerna", because 33 | 34 | - 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. 35 | - Yarn + Lerna seems to be a very good practice used by a lot of other open-source projects to publish npm packages. 36 | 37 | ## Links 38 | 39 | - [A Beginner's Guide to Lerna with Yarn Workspaces](https://medium.com/@jsilvax/a-workflow-guide-for-lerna-with-yarn-workspaces-60f97481149d) 40 | - [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) 41 | -------------------------------------------------------------------------------- /docs/adr/20200924-use-markdown-architectural-decision-records.md: -------------------------------------------------------------------------------- 1 | # Use Markdown Architectural Decision Records 2 | 3 | - Status: accepted 4 | - Date: 2020-09-24 5 | 6 | ## Context and Problem Statement 7 | 8 | We want to record architectural decisions made in this project. 9 | Which format and structure should these records follow? 10 | 11 | ## Considered Options 12 | 13 | - [MADR](https://adr.github.io/madr/) 2.1.2 with Log4brains patch 14 | - [MADR](https://adr.github.io/madr/) 2.1.2 – The original Markdown Architectural Decision Records 15 | - [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" 16 | - [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements 17 | - Other templates listed at 18 | - Formless – No conventions for file format and structure 19 | 20 | ## Decision Outcome 21 | 22 | Chosen option: "MADR 2.1.2 with Log4brains patch", because 23 | 24 | - Implicit assumptions should be made explicit. 25 | Design documentation is important to enable people understanding the decisions later on. 26 | See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). 27 | - The MADR format is lean and fits our development style. 28 | - The MADR structure is comprehensible and facilitates usage & maintenance. 29 | - The MADR project is vivid. 30 | - Version 2.1.2 is the latest one available when starting to document ADRs. 31 | - The Log4brains patch adds more features, like tags. 32 | 33 | The "Log4brains patch" performs the following modifications to the original template: 34 | 35 | - Change the ADR filenames format (`NNN-adr-name` becomes `YYYYMMDD-adr-name`), to avoid conflicts during Git merges. 36 | - Add a `draft` status, to enable collaborative writing. 37 | - Add a `Tags` field. 38 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrTemplate.test.ts: -------------------------------------------------------------------------------- 1 | import { Adr } from "./Adr"; 2 | import { AdrSlug } from "./AdrSlug"; 3 | import { AdrTemplate } from "./AdrTemplate"; 4 | import { MarkdownBody } from "./MarkdownBody"; 5 | import { PackageRef } from "./PackageRef"; 6 | 7 | describe("AdrTemplate", () => { 8 | beforeAll(() => { 9 | Adr.setTz("Etc/UTC"); 10 | }); 11 | afterAll(() => { 12 | Adr.clearTz(); 13 | }); 14 | 15 | const tplMarkdown = `# [short title of solved problem and solution] 16 | 17 | - Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)] 18 | - Deciders: [list everyone involved in the decision] 19 | - Date: [YYYY-MM-DD when the decision was last updated] 20 | 21 | Technical Story: [description | ticket/issue URL] 22 | 23 | ## Context and Problem Statement 24 | 25 | [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.] 26 | `; 27 | 28 | it("creates an ADR from the template", () => { 29 | const template = new AdrTemplate({ 30 | package: new PackageRef("test"), 31 | body: new MarkdownBody(tplMarkdown) 32 | }); 33 | const adr = template.createAdrFromMe( 34 | new AdrSlug("test/20200101-hello-world"), 35 | "Hello World" 36 | ); 37 | expect(adr.slug.value).toEqual("test/20200101-hello-world"); 38 | expect(adr.title).toEqual("Hello World"); 39 | }); 40 | 41 | it("throws when package mismatch in slug", () => { 42 | expect(() => { 43 | const template = new AdrTemplate({ 44 | package: new PackageRef("test"), 45 | body: new MarkdownBody(tplMarkdown) 46 | }); 47 | template.createAdrFromMe( 48 | new AdrSlug("other-package/20200101-hello-world"), 49 | "Hello World" 50 | ); 51 | }).toThrow(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/global-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "log4brains", 3 | "version": "1.1.0", 4 | "description": "Log and publish your architecture decisions (ADR) with Log4brains", 5 | "keywords": [ 6 | "log4brains", 7 | "architecture decision records", 8 | "adr", 9 | "architecture", 10 | "knowledge base", 11 | "documentation", 12 | "docs-as-code", 13 | "markdown", 14 | "static site generator", 15 | "documentation generator", 16 | "tooling" 17 | ], 18 | "author": "Thomas Vaillant ", 19 | "license": "Apache-2.0", 20 | "private": false, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "homepage": "https://github.com/thomvaill/log4brains", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/thomvaill/log4brains", 28 | "directory": "packages/global-cli" 29 | }, 30 | "engines": { 31 | "node": ">=18" 32 | }, 33 | "files": [ 34 | "dist" 35 | ], 36 | "source": "./src/index.ts", 37 | "main": "./dist/index.js", 38 | "module": "./dist/index.module.js", 39 | "types": "./dist/index.d.ts", 40 | "bin": { 41 | "log4brains": "./dist/log4brains" 42 | }, 43 | "scripts": { 44 | "dev": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node watch", 45 | "build": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node && copyfiles -u 1 src/log4brains dist", 46 | "clean": "rimraf ./dist", 47 | "typescript": "tsc --noEmit", 48 | "lint": "eslint . --max-warnings=0", 49 | "prepublishOnly": "cp ../../README.md . && yarn build", 50 | "link": "yarn link", 51 | "unlink": "yarn unlink" 52 | }, 53 | "dependencies": { 54 | "@log4brains/cli": "1.1.0", 55 | "@log4brains/cli-common": "1.1.0", 56 | "@log4brains/core": "1.1.0", 57 | "@log4brains/init": "1.1.0", 58 | "@log4brains/web": "1.1.0", 59 | "chalk": "^4.1.0", 60 | "commander": "^6.1.0" 61 | }, 62 | "devDependencies": { 63 | "copyfiles": "^2.4.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/init/assets/use-markdown-architectural-decision-records.md: -------------------------------------------------------------------------------- 1 | # Use Markdown Architectural Decision Records 2 | 3 | - Status: accepted 4 | - Date: {DATE_YESTERDAY} 5 | - Tags: doc 6 | 7 | ## Context and Problem Statement 8 | 9 | We want to record architectural decisions made in this project. 10 | Which format and structure should these records follow? 11 | 12 | ## Considered Options 13 | 14 | - [MADR](https://adr.github.io/madr/) 2.1.2 with Log4brains patch 15 | - [MADR](https://adr.github.io/madr/) 2.1.2 – The original Markdown Architectural Decision Records 16 | - [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" 17 | - [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements 18 | - Other templates listed at 19 | - Formless – No conventions for file format and structure 20 | 21 | ## Decision Outcome 22 | 23 | Chosen option: "MADR 2.1.2 with Log4brains patch", because 24 | 25 | - Implicit assumptions should be made explicit. 26 | Design documentation is important to enable people understanding the decisions later on. 27 | See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). 28 | - The MADR format is lean and fits our development style. 29 | - The MADR structure is comprehensible and facilitates usage & maintenance. 30 | - The MADR project is vivid. 31 | - Version 2.1.2 is the latest one available when starting to document ADRs. 32 | - The Log4brains patch adds more features, like tags. 33 | 34 | The "Log4brains patch" performs the following modifications to the original template: 35 | 36 | - Change the ADR filenames format (`NNN-adr-name` becomes `YYYYMMDD-adr-name`), to avoid conflicts during Git merges. 37 | - Add a `draft` status, to enable collaborative writing. 38 | - Add a `Tags` field. 39 | 40 | ## Links 41 | 42 | - Relates to [Use Log4brains to manage the ADRs]({LOG4BRAINS_ADR_SLUG}.md) 43 | -------------------------------------------------------------------------------- /docs/adr/20210113-distribute-log4brains-as-a-global-npm-package.md: -------------------------------------------------------------------------------- 1 | # Distribute Log4brains as a global NPM package 2 | 3 | - Status: accepted 4 | - Date: 2021-01-13 5 | 6 | Technical Story: 7 | 8 | ## Context and Problem Statement 9 | 10 | Log4brains (`v1.0.0-beta.4`) installation procedure is currently optimized for JS projects and looks like this: 11 | 12 | - Run `npx init-log4brains` 13 | - Which installs locally `@log4brains/cli` and `@log4brains/web` 14 | - And creates custom entries in `package.json`'s `scripts` section: 15 | - `"log4brains-preview": "log4brains-web preview"` 16 | - `"log4brains-build": "log4brains-web build"` 17 | - `"adr": "log4brains adr"` 18 | 19 | For non-JS projects, you have to install manually the packages and the `npx init-log4brains` script does not work. 20 | 21 | Since Log4brains is intended for all projects, not especially JS ones, we have to make the installation procedure simpler and language-agnostic. 22 | 23 | ## Decision Drivers 24 | 25 | - Simplicity of the installation procedure 26 | - Language agnostic 27 | - Initialization script works on any kind of project 28 | - Faster "getting started" 29 | 30 | ## Decision Outcome 31 | 32 | The new installation procedure is now language agnostic and will be the following: 33 | 34 | ```bash 35 | npm install -g log4brains 36 | log4brains init 37 | ``` 38 | 39 | Log4brains will be distributed as a global NPM package named `log4brains`, which provides a global `log4brains` command. 40 | 41 | - This global package will require the existing `@log4brains/cli` and `@log4brains/web` packages 42 | - `init-log4brains` will be renamed to `@log4brains/init` and required as a dependency 43 | 44 | ### Consequences 45 | 46 | For a JS project, it is now impossible to pin Log4brains to a specific version. 47 | We may implement a [xojs/xo](https://github.com/xojs/xo)-like behavior later: "the CLI will use your local install of XO when available, even when run globally." 48 | 49 | ## Links 50 | 51 | - [Related GitHub issue](https://github.com/thomvaill/log4brains/issues/14#issuecomment-750154907) 52 | -------------------------------------------------------------------------------- /packages/core/src/domain/ValueObjectMap.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | import { ValueObject } from "./ValueObject"; 4 | 5 | export class ValueObjectMap, V> 6 | implements Map { 7 | private readonly map: Map; 8 | 9 | constructor(tuples?: [K, V][]) { 10 | this.map = new Map(tuples); 11 | } 12 | 13 | private getKeyRef(key: K): K | undefined { 14 | // eslint-disable-next-line no-restricted-syntax 15 | for (const i of this.map.keys()) { 16 | if (i.equals(key)) { 17 | return i; 18 | } 19 | } 20 | return undefined; 21 | } 22 | 23 | clear(): void { 24 | this.map.clear(); 25 | } 26 | 27 | delete(key: K): boolean { 28 | const keyRef = this.getKeyRef(key); 29 | if (!keyRef) { 30 | return false; 31 | } 32 | return this.delete(keyRef); 33 | } 34 | 35 | forEach( 36 | callbackfn: (value: V, key: K, map: Map) => void, 37 | thisArg?: any 38 | ): void { 39 | this.map.forEach(callbackfn, thisArg); 40 | } 41 | 42 | get(key: K): V | undefined { 43 | const keyRef = this.getKeyRef(key); 44 | if (!keyRef) { 45 | return undefined; 46 | } 47 | return this.map.get(keyRef); 48 | } 49 | 50 | has(key: K): boolean { 51 | const keyRef = this.getKeyRef(key); 52 | if (!keyRef) { 53 | return false; 54 | } 55 | return this.map.has(keyRef); 56 | } 57 | 58 | set(key: K, value: V): this { 59 | this.map.set(key, value); 60 | return this; 61 | } 62 | 63 | get size(): number { 64 | return this.map.size; 65 | } 66 | 67 | [Symbol.iterator](): IterableIterator<[K, V]> { 68 | return this.map[Symbol.iterator](); 69 | } 70 | 71 | entries(): IterableIterator<[K, V]> { 72 | return this.map.entries(); 73 | } 74 | 75 | keys(): IterableIterator { 76 | return this.map.keys(); 77 | } 78 | 79 | values(): IterableIterator { 80 | return this.map.values(); 81 | } 82 | 83 | get [Symbol.toStringTag](): string { 84 | return this.map[Symbol.toStringTag]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/components/TwoColContent/TwoColContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles, createStyles } from "@material-ui/core/styles"; 3 | import clsx from "clsx"; 4 | import { CustomTheme } from "../../mui"; 5 | 6 | const useStyles = makeStyles((theme: CustomTheme) => 7 | createStyles({ 8 | root: { 9 | display: "flex" 10 | }, 11 | layoutLeftCol: { 12 | flexGrow: 0.5, 13 | [theme.breakpoints.down("md")]: { 14 | display: "none" 15 | } 16 | }, 17 | layoutCenterCol: { 18 | paddingLeft: theme.spacing(2), 19 | paddingRight: theme.spacing(2), 20 | flexGrow: 1, 21 | overflowWrap: "anywhere", 22 | [theme.breakpoints.up("md")]: { 23 | flexGrow: 0, 24 | flexShrink: 0, 25 | flexBasis: theme.custom.layout.centerColBasis, 26 | paddingLeft: theme.custom.layout.centerColPadding, 27 | paddingRight: theme.custom.layout.centerColPadding 28 | }, 29 | "& img": { 30 | maxWidth: "100%" 31 | } 32 | }, 33 | layoutRightCol: { 34 | flexGrow: 1, 35 | flexBasis: theme.custom.layout.rightColBasis, 36 | [theme.breakpoints.down("md")]: { 37 | display: "none" 38 | } 39 | }, 40 | rightCol: { 41 | position: "sticky", 42 | top: theme.spacing(14), // TODO: calculate it based on AdrBrowserLayout's topSpace var 43 | alignSelf: "flex-start", 44 | paddingLeft: theme.spacing(2), 45 | minWidth: "20ch" 46 | } 47 | }) 48 | ); 49 | 50 | type TwoColContentProps = { 51 | className?: string; 52 | children: React.ReactNode; 53 | rightColContent?: React.ReactNode; 54 | }; 55 | 56 | export function TwoColContent({ 57 | className, 58 | children, 59 | rightColContent 60 | }: TwoColContentProps) { 61 | const classes = useStyles(); 62 | 63 | return ( 64 |
65 |
66 |
{children}
67 |
68 | {rightColContent} 69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/scenes/IndexScene/IndexScene.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { makeStyles, createStyles } from "@material-ui/core/styles"; 4 | import { Alert } from "@material-ui/lab"; 5 | import { Typography } from "@material-ui/core"; 6 | import { Log4brainsMode, Log4brainsModeContext } from "../../contexts"; 7 | import { Markdown, TwoColContent } from "../../components"; 8 | // eslint-disable-next-line import/no-cycle 9 | import { ConnectedAdrBrowserLayout } from "../../layouts"; 10 | 11 | export type IndexSceneProps = { 12 | projectName: string; 13 | markdown: string; 14 | l4bVersion: string; 15 | }; 16 | 17 | const useStyles = makeStyles(() => 18 | createStyles({ 19 | previewAlert: { 20 | width: "38ch" 21 | }, 22 | previewAlertMessage: { 23 | paddingTop: 2 24 | } 25 | }) 26 | ); 27 | 28 | export function IndexScene({ projectName, markdown }: IndexSceneProps) { 29 | const classes = useStyles(); 30 | 31 | const mode = React.useContext(Log4brainsModeContext); 32 | 33 | const previewAlert = 34 | mode === Log4brainsMode.preview ? ( 35 | 40 | Preview mode 41 | 42 | Hot Reload is enabled on all pages 43 | 44 | 45 | ) : null; 46 | 47 | return ( 48 | <> 49 | 50 | Architecture knowledge base of {projectName} 51 | 55 | 56 | 57 | {markdown} 58 | 59 | 60 | ); 61 | } 62 | 63 | IndexScene.getLayout = (scene: JSX.Element, sceneProps: IndexSceneProps) => ( 64 | {scene} 65 | ); 66 | -------------------------------------------------------------------------------- /packages/init/assets/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Architecture knowledge base 4 | 5 | Welcome 👋 to the architecture knowledge base of {PROJECT_NAME}. 6 | You will find here all the Architecture Decision Records (ADR) of the project. 7 | 8 | ## Definition and purpose 9 | 10 | > An Architectural Decision (AD) is a software design choice that addresses a functional or non-functional requirement that is architecturally significant. 11 | > 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. 12 | 13 | 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. 14 | Moreover, maintaining this documentation aims at: 15 | 16 | - 🚀 Improving and speeding up the onboarding of a new team member 17 | - 🔭 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)) 18 | - 🤝 Formalizing the decision process of the team 19 | 20 | ## Usage 21 | 22 | This website is automatically updated after a change on the `main` branch of the project's Git repository. 23 | 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. 24 | You can browse the ADRs by using the left menu or the search bar. 25 | 26 | The typical workflow of an ADR is the following: 27 | 28 | ![ADR workflow](/l4b-static/adr-workflow.png) 29 | 30 | The decision process is entirely collaborative and backed by pull requests. 31 | 32 | ## More information 33 | 34 | - [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/develop#readme) 35 | - [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/develop#-what-is-an-adr-and-why-should-you-use-them) 36 | - [ADR GitHub organization](https://adr.github.io/) 37 | -------------------------------------------------------------------------------- /packages/core/src/infrastructure/api/transformers/adr-transformers.ts: -------------------------------------------------------------------------------- 1 | import { Adr, AdrFile } from "@src/adr/domain"; 2 | import { GitRepositoryConfig } from "@src/infrastructure/config"; 3 | import { deepFreeze } from "@src/utils"; 4 | import { AdrDto, AdrDtoStatus } from "../types"; 5 | 6 | function buildViewUrl( 7 | repositoryConfig: GitRepositoryConfig, 8 | file: AdrFile 9 | ): string | undefined { 10 | if (!repositoryConfig.url || !repositoryConfig.viewFileUriPattern) { 11 | return undefined; 12 | } 13 | const uri = repositoryConfig.viewFileUriPattern 14 | .replace("%branch", "master") // TODO: make this customizable, and fix the branch name for the Log4brains repository (develop instead of master) 15 | .replace("%path", file.path.pathRelativeToCwd); 16 | return `${repositoryConfig.url.replace(/\.git$/, "")}${uri}`; 17 | } 18 | 19 | export async function adrToDto( 20 | adr: Adr, 21 | repositoryConfig?: GitRepositoryConfig 22 | ): Promise { 23 | if (!adr.file) { 24 | throw new Error("You are serializing an non-saved ADR"); 25 | } 26 | 27 | const viewUrl = repositoryConfig 28 | ? buildViewUrl(repositoryConfig, adr.file) 29 | : undefined; 30 | 31 | return deepFreeze({ 32 | slug: adr.slug.value, 33 | package: adr.package?.name || null, 34 | title: adr.title || null, 35 | status: adr.status.name as AdrDtoStatus, 36 | supersededBy: adr.superseder?.value || null, 37 | tags: adr.tags, 38 | deciders: adr.deciders, 39 | body: { 40 | rawMarkdown: adr.body.getRawMarkdown(), 41 | enhancedMdx: await adr.getEnhancedMdx() 42 | }, 43 | creationDate: adr.creationDate.toJSON(), 44 | lastEditDate: adr.lastEditDate.toJSON(), 45 | lastEditAuthor: adr.lastEditAuthor.name, 46 | publicationDate: adr.publicationDate?.toJSON() || null, 47 | file: { 48 | relativePath: adr.file.path.pathRelativeToCwd, 49 | absolutePath: adr.file.path.absolutePath 50 | }, 51 | ...(repositoryConfig && repositoryConfig.provider && viewUrl 52 | ? { 53 | repository: { 54 | provider: repositoryConfig.provider, 55 | viewUrl 56 | } 57 | } 58 | : undefined) 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /packages/core/src/adr/domain/AdrSlug.test.ts: -------------------------------------------------------------------------------- 1 | import { AdrFile } from "./AdrFile"; 2 | import { AdrSlug } from "./AdrSlug"; 3 | import { FilesystemPath } from "./FilesystemPath"; 4 | import { PackageRef } from "./PackageRef"; 5 | 6 | describe("AdrSlug", () => { 7 | it("returns the package part", () => { 8 | expect(new AdrSlug("my-package/0001-test").packagePart).toEqual( 9 | "my-package" 10 | ); 11 | expect(new AdrSlug("0001-test").packagePart).toBeUndefined(); 12 | }); 13 | 14 | it("returns the name part", () => { 15 | expect(new AdrSlug("my-package/0001-test").namePart).toEqual("0001-test"); 16 | expect(new AdrSlug("0001-test").namePart).toEqual("0001-test"); 17 | }); 18 | 19 | describe("createFromFile()", () => { 20 | it("creates from AdrFile with package", () => { 21 | expect( 22 | AdrSlug.createFromFile( 23 | new AdrFile(new FilesystemPath("/", "0001-my-adr.md")), 24 | new PackageRef("my-package") 25 | ).value 26 | ).toEqual("my-package/0001-my-adr"); 27 | }); 28 | 29 | it("creates from AdrFile without package", () => { 30 | expect( 31 | AdrSlug.createFromFile( 32 | new AdrFile(new FilesystemPath("/", "0001-my-adr.md")) 33 | ).value 34 | ).toEqual("0001-my-adr"); 35 | }); 36 | }); 37 | 38 | describe("createFromTitle()", () => { 39 | it("creates from title with package", () => { 40 | expect( 41 | AdrSlug.createFromTitle( 42 | "My ADR", 43 | new PackageRef("my-package"), 44 | new Date(2020, 0, 1) 45 | ).value 46 | ).toEqual("my-package/20200101-my-adr"); 47 | }); 48 | 49 | it("creates from title without package", () => { 50 | expect( 51 | AdrSlug.createFromTitle("My ADR", undefined, new Date(2020, 0, 1)).value 52 | ).toEqual("20200101-my-adr"); 53 | }); 54 | 55 | it("creates from title with complex title", () => { 56 | expect( 57 | AdrSlug.createFromTitle( 58 | "L'exemple d'un titre compliqué ! @test", 59 | undefined, 60 | new Date(2020, 0, 1) 61 | ).value 62 | ).toEqual("20200101-lexemple-dun-titre-complique-test"); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | .next-export 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Log4brains 108 | .log4brains 109 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/layouts/AdrBrowserLayout/components/ConnectedSearchBox/ConnectedSearchBox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SearchBox, SearchBoxProps } from "../../../../components/SearchBox"; 3 | import { createSearchInstance } from "../../../../lib/search"; 4 | import { Search, SearchResult } from "../../../../lib-shared/search"; 5 | import { Log4brainsMode, Log4brainsModeContext } from "../../../../contexts"; 6 | 7 | export type ConnectedSearchBoxProps = Omit< 8 | SearchBoxProps, 9 | "onQueryChange" | "query" | "results" | "onFocus" 10 | >; 11 | 12 | export function ConnectedSearchBox(props: ConnectedSearchBoxProps) { 13 | const mode = React.useContext(Log4brainsModeContext); 14 | 15 | const [searchInstance, setSearchInstance] = React.useState(); 16 | const [pendingSearch, setPendingSearchState] = React.useState(false); 17 | const [searchQuery, setSearchQueryState] = React.useState(""); 18 | const [searchResults, setSearchResultsState] = React.useState( 19 | [] 20 | ); 21 | 22 | const handleSearchQueryChange = (query: string): void => { 23 | setSearchQueryState(query); 24 | 25 | if (query.trim() === "") { 26 | setSearchResultsState([]); 27 | return; 28 | } 29 | 30 | if (searchInstance) { 31 | setSearchResultsState(searchInstance.search(query)); 32 | if (pendingSearch) { 33 | setPendingSearchState(false); 34 | } 35 | } else { 36 | setPendingSearchState(true); 37 | } 38 | }; 39 | 40 | const handleFocus = async () => { 41 | // We re-create the search instance on each focus in preview mode 42 | if (!searchInstance || mode === Log4brainsMode.preview) { 43 | setSearchInstance(await createSearchInstance(mode)); 44 | } 45 | }; 46 | 47 | // Trigger a possible pending search after setting the search instance 48 | if (pendingSearch && searchInstance) { 49 | handleSearchQueryChange(searchQuery); 50 | } 51 | 52 | return ( 53 | handleSearchQueryChange(query)} 56 | query={searchQuery} 57 | results={searchResults} 58 | onFocus={handleFocus} 59 | loading={pendingSearch} 60 | /> 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /docs/adr/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Architecture knowledge base 4 | 5 | Welcome 👋 to the architecture knowledge base of Log4brains. 6 | You will find here all the Architecture Decision Records (ADR) of the project. 7 | 8 | ## Definition and purpose 9 | 10 | > An Architectural Decision (AD) is a software design choice that addresses a functional or non-functional requirement that is architecturally significant. 11 | > 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. 12 | 13 | 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. 14 | Moreover, maintaining this documentation aims at: 15 | 16 | - 🚀 Improving and speeding up the onboarding of a new team member 17 | - 🔭 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)) 18 | - 🤝 Formalizing the decision process of the team 19 | 20 | ## Usage 21 | 22 | This website is automatically updated after a change on the `develop` branch of the project's Git repository, from the latest beta version of Log4brains. 23 | 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. 24 | You can browse the ADRs by using the left menu or the search bar. 25 | 26 | The typical workflow of an ADR is the following: 27 | 28 | ![ADR workflow](/l4b-static/adr-workflow.png) 29 | 30 | The decision process is entirely collaborative and backed by pull requests. 31 | 32 | ## More information 33 | 34 | - [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/develop#readme) 35 | - [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/develop#-what-is-an-adr-and-why-should-you-use-them) 36 | - [ADR GitHub organization](https://adr.github.io/) 37 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@log4brains/core", 3 | "version": "1.1.0", 4 | "description": "Log4brains architecture knowledge base core API", 5 | "keywords": [ 6 | "log4brains" 7 | ], 8 | "author": "Thomas Vaillant ", 9 | "license": "Apache-2.0", 10 | "private": false, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "homepage": "https://github.com/thomvaill/log4brains", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/thomvaill/log4brains", 18 | "directory": "packages/core" 19 | }, 20 | "engines": { 21 | "node": ">=18" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "source": "./src/index.ts", 27 | "main": "./dist/index.js", 28 | "module": "./dist/index.module.js", 29 | "types": "./dist/index.d.ts", 30 | "scripts": { 31 | "dev": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node watch", 32 | "build": "microbundle --no-compress --format es,cjs --tsconfig tsconfig.build.json --target node", 33 | "clean": "rimraf ./dist", 34 | "typescript": "tsc --noEmit", 35 | "test": "jest", 36 | "lint": "eslint . --max-warnings=0", 37 | "typedoc": "typedoc --mode library --includeVersion --readme none --theme minimal --excludePrivate --out docs/typedoc src/index.ts", 38 | "prepublishOnly": "yarn build" 39 | }, 40 | "dependencies": { 41 | "awilix": "^4.2.6", 42 | "cheerio": "1.0.0-rc.3", 43 | "chokidar": "^3.4.3", 44 | "core-js": "^3.7.0", 45 | "git-url-parse": "^11.4.0", 46 | "joi": "^17.2.1", 47 | "launch-editor": "^2.2.1", 48 | "lodash": "^4.17.20", 49 | "markdown-it": "^14.1.0", 50 | "markdown-it-source-map": "^0.1.1", 51 | "moment": "^2.29.1", 52 | "moment-timezone": "^0.5.32", 53 | "neverthrow": "^2.7.1", 54 | "open": "^7.3.0", 55 | "parse-git-config": "^3.0.0", 56 | "simple-git": "^2.21.0", 57 | "slugify": "^1.4.5", 58 | "yaml": "^1.10.0" 59 | }, 60 | "devDependencies": { 61 | "@types/cheerio": "^0.22.22", 62 | "@types/git-url-parse": "^9.0.0", 63 | "@types/joi": "^14.3.4", 64 | "@types/lodash": "^4.14.161", 65 | "@types/markdown-it": "^14.1.2", 66 | "@types/parse-git-config": "^3.0.0", 67 | "globby": "^11.0.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/web/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const withBundleAnalyzer = require("@next/bundle-analyzer")({ 5 | enabled: process.env.ANALYZE === "true" 6 | }); 7 | 8 | const packageJson = require(`${ 9 | fs.existsSync(path.join(__dirname, "package.json")) ? "./" : "../" 10 | }package.json`); 11 | 12 | module.exports = withBundleAnalyzer({ 13 | reactStrictMode: true, 14 | target: "serverless", 15 | poweredByHeader: false, 16 | trailingSlash: true, 17 | serverRuntimeConfig: { 18 | PROJECT_ROOT: __dirname, // https://github.com/vercel/next.js/issues/8251 19 | VERSION: packageJson.version 20 | }, 21 | webpack(config, { webpack, buildId }) { 22 | // For cache invalidation purpose (thanks https://github.com/vercel/next.js/discussions/14743) 23 | config.plugins.push( 24 | new webpack.DefinePlugin({ 25 | "process.env.NEXT_BUILD_ID": JSON.stringify(buildId) 26 | }) 27 | ); 28 | 29 | // #NEXTJS-HACK 30 | // Fix when the app is running inside `node_modules` (https://github.com/vercel/next.js/issues/19739) 31 | // TODO: remove this fix when this PR is merged: https://github.com/vercel/next.js/pull/19749 32 | const originalExcludeMethod = config.module.rules[0].exclude; 33 | config.module.rules[0].exclude = (excludePath) => { 34 | if (!originalExcludeMethod(excludePath)) { 35 | return false; 36 | } 37 | return /node_modules/.test(excludePath.replace(config.context, "")); 38 | }; 39 | 40 | // To avoid issues with fsevents during the build, especially on macOS 41 | config.externals.push("chokidar"); 42 | 43 | return config; 44 | }, 45 | future: { 46 | excludeDefaultMomentLocales: true 47 | }, 48 | typescript: { 49 | // We check typescript errors only during the first build, not during "log4brains build", 50 | // for performance purpose and to avoid importing @types/* packages as dependencies 51 | // #NEXTJS-HACK Exception: typescript, @types/react and @types/node are required because of the Next.js verifyTypeScriptSetup() pre-checks 52 | // TODO: in the future, try to compile the Next.js app to JS during the build phase to avoid depending on typescript dependencies at the runtime 53 | ignoreBuildErrors: process.env.LOG4BRAINS_PHASE !== "initial-build" 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /packages/web/nextjs/src/pages/api/adr/[...slugAndMore].ts: -------------------------------------------------------------------------------- 1 | import { Log4brainsError } from "@log4brains/core"; 2 | import { NextApiRequest, NextApiResponse } from "next"; 3 | import { AppConsole } from "@log4brains/cli-common"; 4 | import { getLog4brainsInstance } from "../../../lib/core-api"; 5 | 6 | // TODO: get the global singleton of AppConsole instead of re-creating it 7 | const debug = !!process.env.DEBUG; 8 | const dev = process.env.NODE_ENV === "development"; 9 | const appConsole = new AppConsole({ debug, traces: debug || dev }); 10 | 11 | export default async ( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ): Promise => { 15 | if (!req.query.slugAndMore || !Array.isArray(req.query.slugAndMore)) { 16 | res.status(404).send("Not Found"); 17 | return; 18 | } 19 | const uri = [...req.query.slugAndMore].join("/"); 20 | const l4bInstance = getLog4brainsInstance(); 21 | 22 | // POST /adr/:slug/_open-in-editor 23 | if (req.method === "POST" && uri.endsWith("/_open-in-editor")) { 24 | const slug = uri.replace(/\/_open-in-editor$/, ""); 25 | try { 26 | await l4bInstance.openAdrInEditor(slug, () => { 27 | appConsole.warn("We were not able to detect your preferred editor"); 28 | appConsole.warn( 29 | "You can define it by setting your $VISUAL or $EDITOR environment variable in ~/.zshenv or ~/.bashrc" 30 | ); 31 | }); 32 | res.status(200).send(""); 33 | return; 34 | } catch (e) { 35 | if ( 36 | e instanceof Log4brainsError && 37 | e.name === "This ADR does not exist" 38 | ) { 39 | res.status(404).send("Not Found"); 40 | return; 41 | } 42 | throw e; 43 | } 44 | } 45 | 46 | // GET /adr/:slug 47 | // TODO: remove this dead code when we are sure we don't need this route 48 | 49 | // if (req.method === "GET") { 50 | // const adr = await l4bInstance.getAdrBySlug(uri); 51 | // if (adr) { 52 | // res 53 | // .status(200) 54 | // .json( 55 | // toAdr( 56 | // adr, 57 | // adr.supersededBy 58 | // ? await l4bInstance.getAdrBySlug(adr.supersededBy) 59 | // : undefined 60 | // ) 61 | // ); 62 | // return; 63 | // } 64 | // } 65 | 66 | res.status(404).send("Not Found"); 67 | }; 68 | -------------------------------------------------------------------------------- /e2e-tests/e2e-launcher.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | const rimraf = require("rimraf"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | const fs = require("fs"); 6 | const chalk = require("chalk"); 7 | const { expect } = require("chai"); 8 | 9 | const fsP = fs.promises; 10 | 11 | process.env.NODE_ENV = "test"; 12 | 13 | // Inspired by Next.js's test/integration/create-next-app/index.test.js. Thank you! 14 | async function usingTempDir(fn) { 15 | const folder = await fsP.mkdtemp(path.join(os.tmpdir(), "log4brains-e2e-")); 16 | console.log(chalk.bold(`${chalk.green("WORKDIR")} ${folder}`)); 17 | try { 18 | return await fn(folder); 19 | } finally { 20 | rimraf.sync(folder); 21 | } 22 | } 23 | 24 | async function run(file, arguments, cwd) { 25 | console.log( 26 | chalk.bold(`${chalk.green("RUN")} ${file} ${arguments.join(" ")}`) 27 | ); 28 | const childProcess = execa(file, arguments, { cwd }); 29 | childProcess.stdout.pipe(process.stdout); 30 | childProcess.stderr.pipe(process.stderr); 31 | return await childProcess; 32 | } 33 | 34 | (async () => { 35 | await usingTempDir(async (cwd) => { 36 | await run("log4brains", ["--version"], cwd); 37 | 38 | await run("log4brains", ["init", "--defaults"], cwd); 39 | 40 | await run("log4brains", ["adr", "new", "--quiet", '"E2E test ADR"'], cwd); 41 | 42 | const adrListRes = await run("log4brains", ["adr", "list", "--raw"], cwd); 43 | expect(adrListRes.stdout).to.contain( 44 | "use-log4brains-to-manage-the-adrs", 45 | "Log4brains ADR was not created by init" 46 | ); 47 | expect(adrListRes.stdout).to.contain( 48 | "use-markdown-architectural-decision-records", 49 | "MADR ADR was not created by init" 50 | ); 51 | expect(adrListRes.stdout).to.contain( 52 | "E2E test ADR", 53 | "E2E test ADR was not created" 54 | ); 55 | 56 | await run("log4brains", ["build"], cwd); 57 | expect(fs.existsSync(path.join(cwd, ".log4brains", "out", "index.html"))).to 58 | .be.true; 59 | 60 | // TODO: preview tests (https://github.com/thomvaill/log4brains/issues/2) 61 | 62 | console.log(chalk.bold.green("END")); 63 | }); 64 | })().catch((e) => { 65 | console.error(""); 66 | console.error(`${chalk.red.bold("== FATAL ERROR ==")}`); 67 | console.error(e); 68 | process.exit(1); 69 | }); 70 | --------------------------------------------------------------------------------