From 48f29376eeb24f9c6f91855c76d47d9e2cf48363 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 7 Dec 2023 15:31:58 +0000 Subject: [PATCH] feat(remix): add remix --- CODEOWNERS | 5 + docs/generated/manifests/menus.json | 163 +++ docs/generated/manifests/nx-api.json | 167 +++ docs/generated/packages-metadata.json | 166 +++ .../packages/remix/executors/build.json | 48 + .../packages/remix/executors/serve.json | 51 + .../packages/remix/generators/action.json | 37 + .../remix/generators/application.json | 69 + .../cypress-component-configuration.json | 51 + .../packages/remix/generators/cypress.json | 65 + .../remix/generators/error-boundary.json | 48 + .../packages/remix/generators/library.json | 81 ++ .../packages/remix/generators/loader.json | 37 + .../packages/remix/generators/meta.json | 37 + .../packages/remix/generators/preset.json | 23 + .../remix/generators/resource-route.json | 59 + .../packages/remix/generators/route.json | 70 + .../remix/generators/setup-tailwind.json | 43 + .../packages/remix/generators/setup.json | 23 + .../generators/storybook-configuration.json | 93 ++ .../packages/remix/generators/style.json | 44 + docs/shared/reference/sitemap.md | 20 + e2e/remix/jest.config.ts | 13 + e2e/remix/project.json | 10 + e2e/remix/tests/nx-remix.test.ts | 175 +++ e2e/remix/tsconfig.json | 13 + e2e/remix/tsconfig.spec.json | 20 + e2e/utils/create-project-utils.ts | 1 + package.json | 2 + packages-legacy/remix/README.md | 11 + packages-legacy/remix/generators.json | 4 + packages-legacy/remix/index.ts | 1 + packages-legacy/remix/package.json | 35 + packages-legacy/remix/project.json | 38 + packages-legacy/remix/tsconfig.json | 9 + packages/nx/package.json | 2 + packages/nx/src/utils/plugins/core-plugins.ts | 4 + packages/remix/.eslintrc.json | 46 + packages/remix/README.md | 120 ++ packages/remix/executors.json | 14 + packages/remix/generators.json | 89 ++ packages/remix/index.ts | 15 + packages/remix/jest.config.ts | 15 + packages/remix/migrations.json | 54 + packages/remix/nx-remix.png | Bin 0 -> 205679 bytes packages/remix/package.json | 45 + .../remix/plugins/component-testing/index.ts | 85 ++ packages/remix/project.json | 65 + .../remix/src/executors/build/build.impl.ts | 142 ++ .../remix/src/executors/build/schema.d.ts | 7 + .../remix/src/executors/build/schema.json | 38 + .../remix/src/executors/serve/schema.d.ts | 9 + .../remix/src/executors/serve/schema.json | 41 + .../remix/src/executors/serve/serve.impl.ts | 95 ++ .../src/generators/action/action.impl.spec.ts | 90 ++ .../src/generators/action/action.impl.ts | 48 + .../remix/src/generators/action/schema.d.ts | 10 + .../remix/src/generators/action/schema.json | 32 + .../application.impl.spec.ts.snap | 1213 ++++++++++++++++ .../application/application.impl.spec.ts | 315 ++++ .../application/application.impl.ts | 261 ++++ .../files/common/README.md__tmpl__ | 54 + .../files/common/app/root.tsx__tmpl__ | 32 + .../common/app/routes/_index.spec.tsx__tmpl__ | 16 + .../common/app/routes/_index.tsx__tmpl__ | 32 + .../files/common/public/favicon.ico | Bin 0 -> 16958 bytes .../files/common/remix.config.cjs__tmpl__ | 11 + .../files/common/remix.env.d.ts__tmpl__ | 2 + .../files/common/tsconfig.json__tmpl__ | 18 + .../files/integrated/.gitignore__tmpl__ | 4 + .../files/integrated/package.json__tmpl__ | 28 + .../src/generators/application/lib/index.ts | 2 + .../application/lib/normalize-options.ts | 43 + .../lib/update-unit-test-config.ts | 57 + .../src/generators/application/schema.d.ts | 13 + .../src/generators/application/schema.json | 61 + ...press-component-configuration.impl.spec.ts | 51 + .../cypress-component-configuration.impl.ts | 30 + .../files/cypress.config.ts__tmpl__ | 6 + .../schema.d.ts | 5 + .../schema.json | 41 + .../generators/cypress/cypress.impl.spec.ts | 35 + .../src/generators/cypress/cypress.impl.ts | 113 ++ .../remix/src/generators/cypress/schema.d.ts | 14 + .../remix/src/generators/cypress/schema.json | 60 + .../error-boundary.impl.spec.ts.snap | 61 + .../error-boundary.impl.spec.ts | 67 + .../error-boundary/error-boundary.impl.ts | 16 + .../lib/add-v2-error-boundary.ts | 41 + .../generators/error-boundary/lib/index.ts | 2 + .../error-boundary/lib/normalize-options.ts | 24 + .../src/generators/error-boundary/schema.d.ts | 11 + .../src/generators/error-boundary/schema.json | 40 + .../__snapshots__/library.impl.spec.ts.snap | 157 ++ .../library/lib/add-tsconfig-entry-points.ts | 35 + .../library/lib/add-unit-testing.ts | 62 + .../remix/src/generators/library/lib/index.ts | 4 + .../library/lib/normalize-options.ts | 33 + .../library/lib/update-buildable-config.ts | 25 + .../generators/library/library.impl.spec.ts | 147 ++ .../src/generators/library/library.impl.ts | 49 + .../remix/src/generators/library/schema.d.ts | 15 + .../remix/src/generators/library/schema.json | 73 + .../src/generators/loader/loader.impl.spec.ts | 90 ++ .../src/generators/loader/loader.impl.ts | 48 + .../remix/src/generators/loader/schema.d.ts | 10 + .../remix/src/generators/loader/schema.json | 32 + .../src/generators/meta/lib/v2.impl.spec.ts | 41 + .../remix/src/generators/meta/lib/v2.impl.ts | 36 + .../src/generators/meta/meta.impl.spec.ts | 54 + .../remix/src/generators/meta/meta.impl.ts | 7 + .../remix/src/generators/meta/schema.d.ts | 10 + .../remix/src/generators/meta/schema.json | 32 + .../preset/lib/normalize-options.ts | 32 + .../src/generators/preset/preset.impl.ts | 33 + .../remix/src/generators/preset/schema.d.ts | 4 + .../remix/src/generators/preset/schema.json | 13 + .../resource-route.impl.spec.ts.snap | 21 + .../resource-route.impl.spec.ts | 155 ++ .../resource-route/resource-route.impl.ts | 64 + .../src/generators/resource-route/schema.d.ts | 13 + .../src/generators/resource-route/schema.json | 54 + .../__snapshots__/route.impl.spec.ts.snap | 95 ++ .../src/generators/route/route.impl.spec.ts | 282 ++++ .../remix/src/generators/route/route.impl.ts | 116 ++ .../remix/src/generators/route/schema.d.ts | 15 + .../remix/src/generators/route/schema.json | 65 + .../setup-tailwind.impl.spec.ts.snap | 156 ++ .../files/app/tailwind.css__tpl__ | 3 + .../files/tailwind.config.ts__tpl__ | 13 + .../generators/setup-tailwind/lib/index.ts | 1 + .../lib/update-remix-config.spec.ts | 79 + .../setup-tailwind/lib/update-remix-config.ts | 52 + .../src/generators/setup-tailwind/schema.d.ts | 5 + .../src/generators/setup-tailwind/schema.json | 35 + .../setup-tailwind.impl.spec.ts | 53 + .../setup-tailwind/setup-tailwind.impl.ts | 68 + .../remix/src/generators/setup/schema.json | 13 + .../src/generators/setup/setup.impl.spec.ts | 30 + .../remix/src/generators/setup/setup.impl.ts | 43 + .../storybook-configuration.impl.spec.ts.snap | 85 ++ .../files/vite.config.ts__tpl__ | 15 + .../storybook-configuration/schema.d.ts | 15 + .../storybook-configuration/schema.json | 89 ++ .../storybook-configuration.impl.spec.ts | 33 + .../storybook-configuration.impl.ts | 24 + .../remix/src/generators/style/schema.d.ts | 10 + .../remix/src/generators/style/schema.json | 39 + .../src/generators/style/style.impl.spec.ts | 163 +++ .../remix/src/generators/style/style.impl.ts | 91 ++ .../src/utils/create-watch-paths.spec.ts | 160 ++ .../remix/src/utils/create-watch-paths.ts | 59 + .../src/utils/get-default-export-name.spec.ts | 31 + .../src/utils/get-default-export-name.ts | 6 + .../remix/src/utils/get-default-export.ts | 29 + .../remix/src/utils/insert-import.spec.ts | 93 ++ packages/remix/src/utils/insert-import.ts | 90 ++ .../insert-statement-after-imports.spec.ts | 33 + .../utils/insert-statement-after-imports.ts | 39 + ...sert-statement-in-default-function.spec.ts | 40 + .../insert-statement-in-default-function.ts | 29 + packages/remix/src/utils/remix-config.ts | 19 + packages/remix/src/utils/remix-route-utils.ts | 95 ++ .../remix/src/utils/testing-config-utils.ts | 128 ++ .../src/utils/upsert-links-function.spec.ts | 61 + .../remix/src/utils/upsert-links-function.ts | 51 + packages/remix/src/utils/versions.ts | 29 + packages/remix/tsconfig.json | 13 + packages/remix/tsconfig.lib.json | 12 + packages/remix/tsconfig.spec.json | 21 + pnpm-lock.yaml | 1291 ++++++++++++++++- scripts/commitizen.js | 1 + tsconfig.base.json | 2 + 173 files changed, 10908 insertions(+), 38 deletions(-) create mode 100644 docs/generated/packages/remix/executors/build.json create mode 100644 docs/generated/packages/remix/executors/serve.json create mode 100644 docs/generated/packages/remix/generators/action.json create mode 100644 docs/generated/packages/remix/generators/application.json create mode 100644 docs/generated/packages/remix/generators/cypress-component-configuration.json create mode 100644 docs/generated/packages/remix/generators/cypress.json create mode 100644 docs/generated/packages/remix/generators/error-boundary.json create mode 100644 docs/generated/packages/remix/generators/library.json create mode 100644 docs/generated/packages/remix/generators/loader.json create mode 100644 docs/generated/packages/remix/generators/meta.json create mode 100644 docs/generated/packages/remix/generators/preset.json create mode 100644 docs/generated/packages/remix/generators/resource-route.json create mode 100644 docs/generated/packages/remix/generators/route.json create mode 100644 docs/generated/packages/remix/generators/setup-tailwind.json create mode 100644 docs/generated/packages/remix/generators/setup.json create mode 100644 docs/generated/packages/remix/generators/storybook-configuration.json create mode 100644 docs/generated/packages/remix/generators/style.json create mode 100644 e2e/remix/jest.config.ts create mode 100644 e2e/remix/project.json create mode 100644 e2e/remix/tests/nx-remix.test.ts create mode 100644 e2e/remix/tsconfig.json create mode 100644 e2e/remix/tsconfig.spec.json create mode 100644 packages-legacy/remix/README.md create mode 100644 packages-legacy/remix/generators.json create mode 100644 packages-legacy/remix/index.ts create mode 100644 packages-legacy/remix/package.json create mode 100644 packages-legacy/remix/project.json create mode 100644 packages-legacy/remix/tsconfig.json create mode 100644 packages/remix/.eslintrc.json create mode 100644 packages/remix/README.md create mode 100644 packages/remix/executors.json create mode 100644 packages/remix/generators.json create mode 100644 packages/remix/index.ts create mode 100644 packages/remix/jest.config.ts create mode 100644 packages/remix/migrations.json create mode 100644 packages/remix/nx-remix.png create mode 100644 packages/remix/package.json create mode 100644 packages/remix/plugins/component-testing/index.ts create mode 100644 packages/remix/project.json create mode 100644 packages/remix/src/executors/build/build.impl.ts create mode 100644 packages/remix/src/executors/build/schema.d.ts create mode 100644 packages/remix/src/executors/build/schema.json create mode 100644 packages/remix/src/executors/serve/schema.d.ts create mode 100644 packages/remix/src/executors/serve/schema.json create mode 100644 packages/remix/src/executors/serve/serve.impl.ts create mode 100644 packages/remix/src/generators/action/action.impl.spec.ts create mode 100644 packages/remix/src/generators/action/action.impl.ts create mode 100644 packages/remix/src/generators/action/schema.d.ts create mode 100644 packages/remix/src/generators/action/schema.json create mode 100644 packages/remix/src/generators/application/__snapshots__/application.impl.spec.ts.snap create mode 100644 packages/remix/src/generators/application/application.impl.spec.ts create mode 100644 packages/remix/src/generators/application/application.impl.ts create mode 100644 packages/remix/src/generators/application/files/common/README.md__tmpl__ create mode 100644 packages/remix/src/generators/application/files/common/app/root.tsx__tmpl__ create mode 100644 packages/remix/src/generators/application/files/common/app/routes/_index.spec.tsx__tmpl__ create mode 100644 packages/remix/src/generators/application/files/common/app/routes/_index.tsx__tmpl__ create mode 100644 packages/remix/src/generators/application/files/common/public/favicon.ico create mode 100644 packages/remix/src/generators/application/files/common/remix.config.cjs__tmpl__ create mode 100644 packages/remix/src/generators/application/files/common/remix.env.d.ts__tmpl__ create mode 100644 packages/remix/src/generators/application/files/common/tsconfig.json__tmpl__ create mode 100644 packages/remix/src/generators/application/files/integrated/.gitignore__tmpl__ create mode 100644 packages/remix/src/generators/application/files/integrated/package.json__tmpl__ create mode 100644 packages/remix/src/generators/application/lib/index.ts create mode 100644 packages/remix/src/generators/application/lib/normalize-options.ts create mode 100644 packages/remix/src/generators/application/lib/update-unit-test-config.ts create mode 100644 packages/remix/src/generators/application/schema.d.ts create mode 100644 packages/remix/src/generators/application/schema.json create mode 100644 packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.spec.ts create mode 100644 packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.ts create mode 100644 packages/remix/src/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ create mode 100644 packages/remix/src/generators/cypress-component-configuration/schema.d.ts create mode 100644 packages/remix/src/generators/cypress-component-configuration/schema.json create mode 100644 packages/remix/src/generators/cypress/cypress.impl.spec.ts create mode 100644 packages/remix/src/generators/cypress/cypress.impl.ts create mode 100644 packages/remix/src/generators/cypress/schema.d.ts create mode 100644 packages/remix/src/generators/cypress/schema.json create mode 100644 packages/remix/src/generators/error-boundary/__snapshots__/error-boundary.impl.spec.ts.snap create mode 100644 packages/remix/src/generators/error-boundary/error-boundary.impl.spec.ts create mode 100644 packages/remix/src/generators/error-boundary/error-boundary.impl.ts create mode 100644 packages/remix/src/generators/error-boundary/lib/add-v2-error-boundary.ts create mode 100644 packages/remix/src/generators/error-boundary/lib/index.ts create mode 100644 packages/remix/src/generators/error-boundary/lib/normalize-options.ts create mode 100644 packages/remix/src/generators/error-boundary/schema.d.ts create mode 100644 packages/remix/src/generators/error-boundary/schema.json create mode 100644 packages/remix/src/generators/library/__snapshots__/library.impl.spec.ts.snap create mode 100644 packages/remix/src/generators/library/lib/add-tsconfig-entry-points.ts create mode 100644 packages/remix/src/generators/library/lib/add-unit-testing.ts create mode 100644 packages/remix/src/generators/library/lib/index.ts create mode 100644 packages/remix/src/generators/library/lib/normalize-options.ts create mode 100644 packages/remix/src/generators/library/lib/update-buildable-config.ts create mode 100644 packages/remix/src/generators/library/library.impl.spec.ts create mode 100644 packages/remix/src/generators/library/library.impl.ts create mode 100644 packages/remix/src/generators/library/schema.d.ts create mode 100644 packages/remix/src/generators/library/schema.json create mode 100644 packages/remix/src/generators/loader/loader.impl.spec.ts create mode 100644 packages/remix/src/generators/loader/loader.impl.ts create mode 100644 packages/remix/src/generators/loader/schema.d.ts create mode 100644 packages/remix/src/generators/loader/schema.json create mode 100644 packages/remix/src/generators/meta/lib/v2.impl.spec.ts create mode 100644 packages/remix/src/generators/meta/lib/v2.impl.ts create mode 100644 packages/remix/src/generators/meta/meta.impl.spec.ts create mode 100644 packages/remix/src/generators/meta/meta.impl.ts create mode 100644 packages/remix/src/generators/meta/schema.d.ts create mode 100644 packages/remix/src/generators/meta/schema.json create mode 100644 packages/remix/src/generators/preset/lib/normalize-options.ts create mode 100644 packages/remix/src/generators/preset/preset.impl.ts create mode 100644 packages/remix/src/generators/preset/schema.d.ts create mode 100644 packages/remix/src/generators/preset/schema.json create mode 100644 packages/remix/src/generators/resource-route/__snapshots__/resource-route.impl.spec.ts.snap create mode 100644 packages/remix/src/generators/resource-route/resource-route.impl.spec.ts create mode 100644 packages/remix/src/generators/resource-route/resource-route.impl.ts create mode 100644 packages/remix/src/generators/resource-route/schema.d.ts create mode 100644 packages/remix/src/generators/resource-route/schema.json create mode 100644 packages/remix/src/generators/route/__snapshots__/route.impl.spec.ts.snap create mode 100644 packages/remix/src/generators/route/route.impl.spec.ts create mode 100644 packages/remix/src/generators/route/route.impl.ts create mode 100644 packages/remix/src/generators/route/schema.d.ts create mode 100644 packages/remix/src/generators/route/schema.json create mode 100644 packages/remix/src/generators/setup-tailwind/__snapshots__/setup-tailwind.impl.spec.ts.snap create mode 100644 packages/remix/src/generators/setup-tailwind/files/app/tailwind.css__tpl__ create mode 100644 packages/remix/src/generators/setup-tailwind/files/tailwind.config.ts__tpl__ create mode 100644 packages/remix/src/generators/setup-tailwind/lib/index.ts create mode 100644 packages/remix/src/generators/setup-tailwind/lib/update-remix-config.spec.ts create mode 100644 packages/remix/src/generators/setup-tailwind/lib/update-remix-config.ts create mode 100644 packages/remix/src/generators/setup-tailwind/schema.d.ts create mode 100644 packages/remix/src/generators/setup-tailwind/schema.json create mode 100644 packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.spec.ts create mode 100644 packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.ts create mode 100644 packages/remix/src/generators/setup/schema.json create mode 100644 packages/remix/src/generators/setup/setup.impl.spec.ts create mode 100644 packages/remix/src/generators/setup/setup.impl.ts create mode 100644 packages/remix/src/generators/storybook-configuration/__snapshots__/storybook-configuration.impl.spec.ts.snap create mode 100644 packages/remix/src/generators/storybook-configuration/files/vite.config.ts__tpl__ create mode 100644 packages/remix/src/generators/storybook-configuration/schema.d.ts create mode 100644 packages/remix/src/generators/storybook-configuration/schema.json create mode 100644 packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.spec.ts create mode 100644 packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.ts create mode 100644 packages/remix/src/generators/style/schema.d.ts create mode 100644 packages/remix/src/generators/style/schema.json create mode 100644 packages/remix/src/generators/style/style.impl.spec.ts create mode 100644 packages/remix/src/generators/style/style.impl.ts create mode 100644 packages/remix/src/utils/create-watch-paths.spec.ts create mode 100644 packages/remix/src/utils/create-watch-paths.ts create mode 100644 packages/remix/src/utils/get-default-export-name.spec.ts create mode 100644 packages/remix/src/utils/get-default-export-name.ts create mode 100644 packages/remix/src/utils/get-default-export.ts create mode 100644 packages/remix/src/utils/insert-import.spec.ts create mode 100644 packages/remix/src/utils/insert-import.ts create mode 100644 packages/remix/src/utils/insert-statement-after-imports.spec.ts create mode 100644 packages/remix/src/utils/insert-statement-after-imports.ts create mode 100644 packages/remix/src/utils/insert-statement-in-default-function.spec.ts create mode 100644 packages/remix/src/utils/insert-statement-in-default-function.ts create mode 100644 packages/remix/src/utils/remix-config.ts create mode 100644 packages/remix/src/utils/remix-route-utils.ts create mode 100644 packages/remix/src/utils/testing-config-utils.ts create mode 100644 packages/remix/src/utils/upsert-links-function.spec.ts create mode 100644 packages/remix/src/utils/upsert-links-function.ts create mode 100644 packages/remix/src/utils/versions.ts create mode 100644 packages/remix/tsconfig.json create mode 100644 packages/remix/tsconfig.lib.json create mode 100644 packages/remix/tsconfig.spec.json diff --git a/CODEOWNERS b/CODEOWNERS index 2147d75575aa62..565ccadc97a7b5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -55,6 +55,11 @@ rust-toolchain @nrwl/nx-native-reviewers /packages/react-native/** @nrwl/nx-react-reviewers /e2e/react-native/** @nrwl/nx-react-reviewers +## remix +/docs/generated/packages/remix/** @nrwl/nx-react-reviewers @nrwl/nx-docs-reviewers @Coly010 +/packages/remix/** @nrwl/nx-react-reviewers @Coly010 +/e2e/remix/** @nrwl/nx-react-reviewers @Coly010 + # Vue /packages/vue/** @nrwl/nx-vue-reviewers /e2e/vue/** @nrwl/nx-vue-reviewers diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 981313f97e780d..6727b1238fd3e4 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -8882,6 +8882,169 @@ "isExternal": false, "disableCollapsible": false }, + { + "id": "remix", + "path": "/nx-api/remix", + "name": "remix", + "children": [ + { + "id": "executors", + "path": "/nx-api/remix/executors", + "name": "executors", + "children": [ + { + "id": "serve", + "path": "/nx-api/remix/executors/serve", + "name": "serve", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "build", + "path": "/nx-api/remix/executors/build", + "name": "build", + "children": [], + "isExternal": false, + "disableCollapsible": false + } + ], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "generators", + "path": "/nx-api/remix/generators", + "name": "generators", + "children": [ + { + "id": "preset", + "path": "/nx-api/remix/generators/preset", + "name": "preset", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "setup", + "path": "/nx-api/remix/generators/setup", + "name": "setup", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "application", + "path": "/nx-api/remix/generators/application", + "name": "application", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "cypress-component-configuration", + "path": "/nx-api/remix/generators/cypress-component-configuration", + "name": "cypress-component-configuration", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "library", + "path": "/nx-api/remix/generators/library", + "name": "library", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "route", + "path": "/nx-api/remix/generators/route", + "name": "route", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "resource-route", + "path": "/nx-api/remix/generators/resource-route", + "name": "resource-route", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "action", + "path": "/nx-api/remix/generators/action", + "name": "action", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "loader", + "path": "/nx-api/remix/generators/loader", + "name": "loader", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "style", + "path": "/nx-api/remix/generators/style", + "name": "style", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "setup-tailwind", + "path": "/nx-api/remix/generators/setup-tailwind", + "name": "setup-tailwind", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "storybook-configuration", + "path": "/nx-api/remix/generators/storybook-configuration", + "name": "storybook-configuration", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "meta", + "path": "/nx-api/remix/generators/meta", + "name": "meta", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "error-boundary", + "path": "/nx-api/remix/generators/error-boundary", + "name": "error-boundary", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "cypress", + "path": "/nx-api/remix/generators/cypress", + "name": "cypress", + "children": [], + "isExternal": false, + "disableCollapsible": false + } + ], + "isExternal": false, + "disableCollapsible": false + } + ], + "isExternal": false, + "disableCollapsible": false + }, { "id": "rollup", "path": "/nx-api/rollup", diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index df66aa5f7560ed..ed18964aa87033 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2362,6 +2362,173 @@ }, "path": "/nx-api/react-native" }, + "remix": { + "githubRoot": "https://github.com/nrwl/nx/blob/master", + "name": "remix", + "packageName": "@nx/remix", + "description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.", + "documents": {}, + "root": "/packages/remix", + "source": "/packages/remix/src", + "executors": { + "/nx-api/remix/executors/serve": { + "description": "Serve a Remix application.", + "file": "generated/packages/remix/executors/serve.json", + "hidden": false, + "name": "serve", + "originalFilePath": "/packages/remix/src/executors/serve/schema.json", + "path": "/nx-api/remix/executors/serve", + "type": "executor" + }, + "/nx-api/remix/executors/build": { + "description": "Build a Remix application.", + "file": "generated/packages/remix/executors/build.json", + "hidden": false, + "name": "build", + "originalFilePath": "/packages/remix/src/executors/build/schema.json", + "path": "/nx-api/remix/executors/build", + "type": "executor" + } + }, + "generators": { + "/nx-api/remix/generators/preset": { + "description": "Generate a new Remix workspace", + "file": "generated/packages/remix/generators/preset.json", + "hidden": true, + "name": "preset", + "originalFilePath": "/packages/remix/src/generators/preset/schema.json", + "path": "/nx-api/remix/generators/preset", + "type": "generator" + }, + "/nx-api/remix/generators/setup": { + "description": "Setup a Remix in an existing workspace", + "file": "generated/packages/remix/generators/setup.json", + "hidden": true, + "name": "setup", + "originalFilePath": "/packages/remix/src/generators/setup/schema.json", + "path": "/nx-api/remix/generators/setup", + "type": "generator" + }, + "/nx-api/remix/generators/application": { + "description": "Generate a new Remix application", + "file": "generated/packages/remix/generators/application.json", + "hidden": false, + "name": "application", + "originalFilePath": "/packages/remix/src/generators/application/schema.json", + "path": "/nx-api/remix/generators/application", + "type": "generator" + }, + "/nx-api/remix/generators/cypress-component-configuration": { + "description": "Generate a Cypress Component Testing configuration for a Remix project", + "file": "generated/packages/remix/generators/cypress-component-configuration.json", + "hidden": false, + "name": "cypress-component-configuration", + "originalFilePath": "/packages/remix/src/generators/cypress-component-configuration/schema.json", + "path": "/nx-api/remix/generators/cypress-component-configuration", + "type": "generator" + }, + "/nx-api/remix/generators/library": { + "description": "Generate a new library", + "file": "generated/packages/remix/generators/library.json", + "hidden": false, + "name": "library", + "originalFilePath": "/packages/remix/src/generators/library/schema.json", + "path": "/nx-api/remix/generators/library", + "type": "generator" + }, + "/nx-api/remix/generators/route": { + "description": "Generate a new route", + "file": "generated/packages/remix/generators/route.json", + "hidden": false, + "name": "route", + "originalFilePath": "/packages/remix/src/generators/route/schema.json", + "path": "/nx-api/remix/generators/route", + "type": "generator" + }, + "/nx-api/remix/generators/resource-route": { + "description": "Generate a new resource route", + "file": "generated/packages/remix/generators/resource-route.json", + "hidden": false, + "name": "resource-route", + "originalFilePath": "/packages/remix/src/generators/resource-route/schema.json", + "path": "/nx-api/remix/generators/resource-route", + "type": "generator" + }, + "/nx-api/remix/generators/action": { + "description": "Add an action function to an existing route", + "file": "generated/packages/remix/generators/action.json", + "hidden": false, + "name": "action", + "originalFilePath": "/packages/remix/src/generators/action/schema.json", + "path": "/nx-api/remix/generators/action", + "type": "generator" + }, + "/nx-api/remix/generators/loader": { + "description": "Add a loader function to an existing route", + "file": "generated/packages/remix/generators/loader.json", + "hidden": false, + "name": "loader", + "originalFilePath": "/packages/remix/src/generators/loader/schema.json", + "path": "/nx-api/remix/generators/loader", + "type": "generator" + }, + "/nx-api/remix/generators/style": { + "description": "Generates a new stylesheet and adds it to an existing route", + "file": "generated/packages/remix/generators/style.json", + "hidden": false, + "name": "style", + "originalFilePath": "/packages/remix/src/generators/style/schema.json", + "path": "/nx-api/remix/generators/style", + "type": "generator" + }, + "/nx-api/remix/generators/setup-tailwind": { + "description": "Generates a TailwindCSS configuration for the Remix application", + "file": "generated/packages/remix/generators/setup-tailwind.json", + "hidden": false, + "name": "setup-tailwind", + "originalFilePath": "/packages/remix/src/generators/setup-tailwind/schema.json", + "path": "/nx-api/remix/generators/setup-tailwind", + "type": "generator" + }, + "/nx-api/remix/generators/storybook-configuration": { + "description": "Generates a Storybook configuration for a Remix application", + "file": "generated/packages/remix/generators/storybook-configuration.json", + "hidden": false, + "name": "storybook-configuration", + "originalFilePath": "/packages/remix/src/generators/storybook-configuration/schema.json", + "path": "/nx-api/remix/generators/storybook-configuration", + "type": "generator" + }, + "/nx-api/remix/generators/meta": { + "description": "Add a meta function to an existing route", + "file": "generated/packages/remix/generators/meta.json", + "hidden": false, + "name": "meta", + "originalFilePath": "/packages/remix/src/generators/meta/schema.json", + "path": "/nx-api/remix/generators/meta", + "type": "generator" + }, + "/nx-api/remix/generators/error-boundary": { + "description": "Add an ErrorBoundary to an existing route", + "file": "generated/packages/remix/generators/error-boundary.json", + "hidden": false, + "name": "error-boundary", + "originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json", + "path": "/nx-api/remix/generators/error-boundary", + "type": "generator" + }, + "/nx-api/remix/generators/cypress": { + "description": "Generate a project for testing Remix apps using Cypress", + "file": "generated/packages/remix/generators/cypress.json", + "hidden": false, + "name": "cypress", + "originalFilePath": "/packages/remix/src/generators/cypress/schema.json", + "path": "/nx-api/remix/generators/cypress", + "type": "generator" + } + }, + "path": "/nx-api/remix" + }, "rollup": { "githubRoot": "https://github.com/nrwl/nx/blob/master", "name": "rollup", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index bec0b277800c02..1431293f15e9cc 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2341,6 +2341,172 @@ "root": "/packages/react-native", "source": "/packages/react-native/src" }, + { + "description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.", + "documents": [], + "executors": [ + { + "description": "Serve a Remix application.", + "file": "generated/packages/remix/executors/serve.json", + "hidden": false, + "name": "serve", + "originalFilePath": "/packages/remix/src/executors/serve/schema.json", + "path": "remix/executors/serve", + "type": "executor" + }, + { + "description": "Build a Remix application.", + "file": "generated/packages/remix/executors/build.json", + "hidden": false, + "name": "build", + "originalFilePath": "/packages/remix/src/executors/build/schema.json", + "path": "remix/executors/build", + "type": "executor" + } + ], + "generators": [ + { + "description": "Generate a new Remix workspace", + "file": "generated/packages/remix/generators/preset.json", + "hidden": true, + "name": "preset", + "originalFilePath": "/packages/remix/src/generators/preset/schema.json", + "path": "remix/generators/preset", + "type": "generator" + }, + { + "description": "Setup a Remix in an existing workspace", + "file": "generated/packages/remix/generators/setup.json", + "hidden": true, + "name": "setup", + "originalFilePath": "/packages/remix/src/generators/setup/schema.json", + "path": "remix/generators/setup", + "type": "generator" + }, + { + "description": "Generate a new Remix application", + "file": "generated/packages/remix/generators/application.json", + "hidden": false, + "name": "application", + "originalFilePath": "/packages/remix/src/generators/application/schema.json", + "path": "remix/generators/application", + "type": "generator" + }, + { + "description": "Generate a Cypress Component Testing configuration for a Remix project", + "file": "generated/packages/remix/generators/cypress-component-configuration.json", + "hidden": false, + "name": "cypress-component-configuration", + "originalFilePath": "/packages/remix/src/generators/cypress-component-configuration/schema.json", + "path": "remix/generators/cypress-component-configuration", + "type": "generator" + }, + { + "description": "Generate a new library", + "file": "generated/packages/remix/generators/library.json", + "hidden": false, + "name": "library", + "originalFilePath": "/packages/remix/src/generators/library/schema.json", + "path": "remix/generators/library", + "type": "generator" + }, + { + "description": "Generate a new route", + "file": "generated/packages/remix/generators/route.json", + "hidden": false, + "name": "route", + "originalFilePath": "/packages/remix/src/generators/route/schema.json", + "path": "remix/generators/route", + "type": "generator" + }, + { + "description": "Generate a new resource route", + "file": "generated/packages/remix/generators/resource-route.json", + "hidden": false, + "name": "resource-route", + "originalFilePath": "/packages/remix/src/generators/resource-route/schema.json", + "path": "remix/generators/resource-route", + "type": "generator" + }, + { + "description": "Add an action function to an existing route", + "file": "generated/packages/remix/generators/action.json", + "hidden": false, + "name": "action", + "originalFilePath": "/packages/remix/src/generators/action/schema.json", + "path": "remix/generators/action", + "type": "generator" + }, + { + "description": "Add a loader function to an existing route", + "file": "generated/packages/remix/generators/loader.json", + "hidden": false, + "name": "loader", + "originalFilePath": "/packages/remix/src/generators/loader/schema.json", + "path": "remix/generators/loader", + "type": "generator" + }, + { + "description": "Generates a new stylesheet and adds it to an existing route", + "file": "generated/packages/remix/generators/style.json", + "hidden": false, + "name": "style", + "originalFilePath": "/packages/remix/src/generators/style/schema.json", + "path": "remix/generators/style", + "type": "generator" + }, + { + "description": "Generates a TailwindCSS configuration for the Remix application", + "file": "generated/packages/remix/generators/setup-tailwind.json", + "hidden": false, + "name": "setup-tailwind", + "originalFilePath": "/packages/remix/src/generators/setup-tailwind/schema.json", + "path": "remix/generators/setup-tailwind", + "type": "generator" + }, + { + "description": "Generates a Storybook configuration for a Remix application", + "file": "generated/packages/remix/generators/storybook-configuration.json", + "hidden": false, + "name": "storybook-configuration", + "originalFilePath": "/packages/remix/src/generators/storybook-configuration/schema.json", + "path": "remix/generators/storybook-configuration", + "type": "generator" + }, + { + "description": "Add a meta function to an existing route", + "file": "generated/packages/remix/generators/meta.json", + "hidden": false, + "name": "meta", + "originalFilePath": "/packages/remix/src/generators/meta/schema.json", + "path": "remix/generators/meta", + "type": "generator" + }, + { + "description": "Add an ErrorBoundary to an existing route", + "file": "generated/packages/remix/generators/error-boundary.json", + "hidden": false, + "name": "error-boundary", + "originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json", + "path": "remix/generators/error-boundary", + "type": "generator" + }, + { + "description": "Generate a project for testing Remix apps using Cypress", + "file": "generated/packages/remix/generators/cypress.json", + "hidden": false, + "name": "cypress", + "originalFilePath": "/packages/remix/src/generators/cypress/schema.json", + "path": "remix/generators/cypress", + "type": "generator" + } + ], + "githubRoot": "https://github.com/nrwl/nx/blob/master", + "name": "remix", + "packageName": "@nx/remix", + "root": "/packages/remix", + "source": "/packages/remix/src" + }, { "description": "The Nx Plugin for Rollup contains executors and generators that support building applications using Rollup.", "documents": [], diff --git a/docs/generated/packages/remix/executors/build.json b/docs/generated/packages/remix/executors/build.json new file mode 100644 index 00000000000000..7dbc7c5f72f8b7 --- /dev/null +++ b/docs/generated/packages/remix/executors/build.json @@ -0,0 +1,48 @@ +{ + "name": "build", + "implementation": "/packages/remix/src/executors/build/build.impl.ts", + "schema": { + "version": 2, + "outputCapture": "pipe", + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "title": "Remix Build", + "description": "Build a Remix app.", + "type": "object", + "properties": { + "outputPath": { + "type": "string", + "description": "The output path of the generated files.", + "x-completion-type": "directory", + "x-priority": "important" + }, + "includeDevDependenciesInPackageJson": { + "type": "boolean", + "description": "Include `devDependencies` in the generated package.json file. By default only production `dependencies` are included.", + "default": false + }, + "generatePackageJson": { + "type": "boolean", + "description": "Generate package.json file in the output folder.", + "default": false + }, + "generateLockfile": { + "type": "boolean", + "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.", + "default": false + }, + "sourcemap": { + "type": "boolean", + "description": "Generate source maps for production.", + "default": false + } + }, + "required": ["outputPath"], + "presets": [] + }, + "description": "Build a Remix application.", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/executors/build/schema.json", + "type": "executor" +} diff --git a/docs/generated/packages/remix/executors/serve.json b/docs/generated/packages/remix/executors/serve.json new file mode 100644 index 00000000000000..c86d2b2e773ef0 --- /dev/null +++ b/docs/generated/packages/remix/executors/serve.json @@ -0,0 +1,51 @@ +{ + "name": "serve", + "implementation": "/packages/remix/src/executors/serve/serve.impl.ts", + "schema": { + "version": 2, + "outputCapture": "pipe", + "cli": "nx", + "title": "Remix Serve", + "description": "Serve a Remix app.", + "type": "object", + "properties": { + "port": { + "type": "number", + "description": "Set PORT environment variable that can be used to serve the Remix application.", + "default": 4200 + }, + "devServerPort": { + "type": "number", + "description": "Port to start the dev server on." + }, + "debug": { + "type": "boolean", + "description": "Attach a Node.js inspector.", + "default": false + }, + "command": { + "type": "string", + "description": "Command used to run your app server." + }, + "manual": { + "type": "boolean", + "description": "Enable manual mode", + "default": false + }, + "tlsKey": { + "type": "string", + "description": "Path to TLS key (key.pem)." + }, + "tlsCert": { + "type": "string", + "description": "Path to TLS certificate (cert.pem)." + } + }, + "presets": [] + }, + "description": "Serve a Remix application.", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/executors/serve/schema.json", + "type": "executor" +} diff --git a/docs/generated/packages/remix/generators/action.json b/docs/generated/packages/remix/generators/action.json new file mode 100644 index 00000000000000..cd3975fdb07bcb --- /dev/null +++ b/docs/generated/packages/remix/generators/action.json @@ -0,0 +1,37 @@ +{ + "name": "action", + "implementation": "/packages/remix/src/generators/action/action.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "action", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route.", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the action in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "projectName" }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + } + }, + "required": ["path"], + "presets": [] + }, + "description": "Add an action function to an existing route", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/action/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/application.json b/docs/generated/packages/remix/generators/application.json new file mode 100644 index 00000000000000..41f65e5b2c8a0f --- /dev/null +++ b/docs/generated/packages/remix/generators/application.json @@ -0,0 +1,69 @@ +{ + "name": "application", + "implementation": "/packages/remix/src/generators/application/application.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixApplication", + "title": "Create an Application", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What is the name of the application?" + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files.", + "default": false + }, + "directory": { + "type": "string", + "description": "A directory where the app is placed.", + "alias": "dir", + "x-priority": "important" + }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "unitTestRunner": { + "type": "string", + "enum": ["vitest", "jest", "none"], + "default": "vitest", + "description": "Test runner to use for unit tests.", + "x-prompt": "What unit test runner should be used?" + }, + "e2eTestRunner": { + "type": "string", + "enum": ["cypress", "none"], + "default": "cypress", + "description": "Test runner to use for e2e tests" + }, + "tags": { + "type": "string", + "description": "Add tags to the project (used for linting)", + "alias": "t" + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files", + "default": false + }, + "rootProject": { + "type": "boolean", + "x-priority": "internal", + "default": false + } + }, + "presets": [] + }, + "description": "Generate a new Remix application", + "aliases": ["app"], + "x-type": "application", + "hidden": false, + "path": "/packages/remix/src/generators/application/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/cypress-component-configuration.json b/docs/generated/packages/remix/generators/cypress-component-configuration.json new file mode 100644 index 00000000000000..9f676dbbc5f4a7 --- /dev/null +++ b/docs/generated/packages/remix/generators/cypress-component-configuration.json @@ -0,0 +1,51 @@ +{ + "name": "cypress-component-configuration", + "implementation": "/packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.ts", + "schema": { + "$schema": "https://json-schema.org/schema", + "cli": "nx", + "$id": "NxRemixCypressComponentTestConfiguration", + "title": "Add Cypress component testing", + "description": "Add a Cypress component testing configuration to an existing project.", + "type": "object", + "examples": [ + { + "command": "nx g @nx/remix:cypress-component-configuration --project=my-remix-project", + "description": "Add component testing to your Remix project" + }, + { + "command": "nx g @nx/remix:cypress-component-configuration --project=my-remix-project --generate-tests", + "description": "Add component testing to your Remix project and generate component tests for your existing components" + } + ], + "properties": { + "project": { + "type": "string", + "description": "The name of the project to add cypress component testing configuration to", + "x-dropdown": "projects", + "x-prompt": "What project should we add Cypress component testing to?", + "x-priority": "important" + }, + "generateTests": { + "type": "boolean", + "description": "Generate default component tests for existing components in the project", + "x-prompt": "Automatically generate tests for components declared in this project?", + "default": false, + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files", + "default": false, + "x-priority": "internal" + } + }, + "required": ["project"], + "presets": [] + }, + "description": "Generate a Cypress Component Testing configuration for a Remix project", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/cypress-component-configuration/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/cypress.json b/docs/generated/packages/remix/generators/cypress.json new file mode 100644 index 00000000000000..a3ba69be057eb7 --- /dev/null +++ b/docs/generated/packages/remix/generators/cypress.json @@ -0,0 +1,65 @@ +{ + "name": "cypress", + "implementation": "/packages/remix/src/generators/cypress/cypress.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixCypress", + "title": "", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the frontend project to test.", + "$default": { "$source": "projectName" } + }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "baseUrl": { + "type": "string", + "description": "URL to access the application on", + "default": "http://localhost:3000" + }, + "name": { + "type": "string", + "description": "Name of the E2E Project", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What name would you like to use for the e2e project?" + }, + "directory": { + "type": "string", + "description": "A directory where the project is placed" + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "none"], + "default": "eslint" + }, + "js": { + "description": "Generate JavaScript files rather than TypeScript files", + "type": "boolean", + "default": false + }, + "skipFormat": { + "description": "Skip formatting files", + "type": "boolean", + "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false + } + }, + "required": ["name"], + "presets": [] + }, + "description": "Generate a project for testing Remix apps using Cypress", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/cypress/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/error-boundary.json b/docs/generated/packages/remix/generators/error-boundary.json new file mode 100644 index 00000000000000..3a742d68658cea --- /dev/null +++ b/docs/generated/packages/remix/generators/error-boundary.json @@ -0,0 +1,48 @@ +{ + "name": "error-boundary", + "implementation": "/packages/remix/src/generators/error-boundary/error-boundary.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixErrorBoundary", + "title": "Create an ErrorBoundary for a Route", + "type": "object", + "examples": [ + { + "command": "g error-boundary --routePath=apps/demo/app/routes/my-route.tsx", + "description": "Generate an ErrorBoundary for my-route.tsx" + } + ], + "properties": { + "path": { + "type": "string", + "description": "The path to route file relative to the project root." + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the error boundary in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "projectName" }, + "x-prompt": "What project contains the route file that this ErrorBoundary is for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files after generation.", + "default": false, + "x-priority": "internal" + } + }, + "required": ["path"], + "presets": [] + }, + "description": "Add an ErrorBoundary to an existing route", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/error-boundary/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/library.json b/docs/generated/packages/remix/generators/library.json new file mode 100644 index 00000000000000..3a256c38527872 --- /dev/null +++ b/docs/generated/packages/remix/generators/library.json @@ -0,0 +1,81 @@ +{ + "name": "library", + "implementation": "/packages/remix/src/generators/library/library.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixLibrary", + "title": "Create a Library", + "type": "object", + "examples": [ + { + "command": "g lib mylib --directory=myapp", + "description": "Generate libs/myapp/mylib" + } + ], + "properties": { + "name": { + "type": "string", + "description": "Library name", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What name would you like to use for the library?", + "pattern": "^[a-zA-Z].*$" + }, + "directory": { + "type": "string", + "description": "A directory where the lib is placed.", + "alias": "dir", + "x-priority": "important" + }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "tags": { + "type": "string", + "description": "Add tags to the library (used for linting)" + }, + "style": { + "type": "string", + "description": "Generate a stylesheet", + "enum": ["none", "css"], + "default": "css" + }, + "buildable": { + "type": "boolean", + "description": "Should the library be buildable?", + "default": false + }, + "unitTestRunner": { + "type": "string", + "enum": ["jest", "vitest", "none"], + "description": "Test Runner to use for Unit Tests", + "x-prompt": "What test runner should be used?", + "default": "vitest" + }, + "importPath": { + "type": "string", + "description": "The library name used to import it, like @myorg/my-awesome-lib" + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files", + "default": false + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files after generator runs", + "default": false, + "x-priority": "internal" + } + }, + "required": ["name"], + "presets": [] + }, + "description": "Generate a new library", + "aliases": ["lib"], + "x-type": "library", + "hidden": false, + "path": "/packages/remix/src/generators/library/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/loader.json b/docs/generated/packages/remix/generators/loader.json new file mode 100644 index 00000000000000..14515acf388117 --- /dev/null +++ b/docs/generated/packages/remix/generators/loader.json @@ -0,0 +1,37 @@ +{ + "name": "loader", + "implementation": "/packages/remix/src/generators/loader/loader.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "data-loader", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route.", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the loader in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "projectName" }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + } + }, + "required": ["path"], + "presets": [] + }, + "description": "Add a loader function to an existing route", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/loader/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/meta.json b/docs/generated/packages/remix/generators/meta.json new file mode 100644 index 00000000000000..ca83d6a98b06f6 --- /dev/null +++ b/docs/generated/packages/remix/generators/meta.json @@ -0,0 +1,37 @@ +{ + "name": "meta", + "implementation": "/packages/remix/src/generators/meta/meta.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "meta", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route.", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the meta function in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "projectName" }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + } + }, + "required": ["path"], + "presets": [] + }, + "description": "Add a meta function to an existing route", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/meta/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/preset.json b/docs/generated/packages/remix/generators/preset.json new file mode 100644 index 00000000000000..b120dcb8e88337 --- /dev/null +++ b/docs/generated/packages/remix/generators/preset.json @@ -0,0 +1,23 @@ +{ + "name": "preset", + "implementation": "/packages/remix/src/generators/preset/preset.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "Remix", + "title": "", + "type": "object", + "properties": { + "tags": { + "type": "string", + "description": "Add tags to the app (used for linting).", + "alias": "t" + } + }, + "presets": [] + }, + "description": "Generate a new Remix workspace", + "hidden": true, + "aliases": [], + "path": "/packages/remix/src/generators/preset/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/resource-route.json b/docs/generated/packages/remix/generators/resource-route.json new file mode 100644 index 00000000000000..26c86fbeb31ce4 --- /dev/null +++ b/docs/generated/packages/remix/generators/resource-route.json @@ -0,0 +1,59 @@ +{ + "name": "resource-route", + "implementation": "/packages/remix/src/generators/resource-route/resource-route.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixResourceRoute", + "title": "Create a Resource Route", + "type": "object", + "examples": [ + { + "command": "g resource-route 'path/to/page'", + "description": "Generate resource route at /path/to/page" + } + ], + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route.", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the styles in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "projectName" }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + }, + "action": { + "type": "boolean", + "description": "Generate an action function", + "default": false + }, + "loader": { + "type": "boolean", + "description": "Generate a loader function", + "default": true + }, + "skipChecks": { + "type": "boolean", + "description": "Skip route error detection", + "default": false + } + }, + "required": ["path"], + "presets": [] + }, + "description": "Generate a new resource route", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/resource-route/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/route.json b/docs/generated/packages/remix/generators/route.json new file mode 100644 index 00000000000000..79a2739cb36e89 --- /dev/null +++ b/docs/generated/packages/remix/generators/route.json @@ -0,0 +1,70 @@ +{ + "name": "route", + "implementation": "/packages/remix/src/generators/route/route.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixRoute", + "title": "Create a Route", + "type": "object", + "examples": [ + { + "command": "g route 'path/to/page'", + "description": "Generate route at /path/to/page" + } + ], + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the workspace root.", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the route in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and path relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "projectName" }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + }, + "style": { + "type": "string", + "description": "Generate a stylesheet", + "enum": ["none", "css"], + "default": "css" + }, + "meta": { + "type": "boolean", + "description": "Generate a meta function", + "default": false + }, + "action": { + "type": "boolean", + "description": "Generate an action function", + "default": false + }, + "loader": { + "type": "boolean", + "description": "Generate a loader function", + "default": false + }, + "skipChecks": { + "type": "boolean", + "description": "Skip route error detection", + "default": false + } + }, + "required": ["path"], + "presets": [] + }, + "description": "Generate a new route", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/route/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/setup-tailwind.json b/docs/generated/packages/remix/generators/setup-tailwind.json new file mode 100644 index 00000000000000..567b43053671fd --- /dev/null +++ b/docs/generated/packages/remix/generators/setup-tailwind.json @@ -0,0 +1,43 @@ +{ + "name": "setup-tailwind", + "implementation": "/packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixTailwind", + "title": "Add TailwindCSS to a Remix App", + "type": "object", + "examples": [ + { + "command": "g setup-tailwind --project=myapp", + "description": "Generate a TailwindCSS config for your Remix app" + } + ], + "properties": { + "project": { + "type": "string", + "description": "The name of the project to add tailwind to", + "$default": { "$source": "projectName" }, + "x-prompt": "What project would you like to add Tailwind to?", + "pattern": "^[a-zA-Z].*$" + }, + "js": { + "type": "boolean", + "description": "Generate a JavaScript config file instead of a TypeScript config file", + "default": false + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files after generator runs", + "default": false, + "x-priority": "internal" + } + }, + "required": ["project"], + "presets": [] + }, + "description": "Generates a TailwindCSS configuration for the Remix application", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/setup-tailwind/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/setup.json b/docs/generated/packages/remix/generators/setup.json new file mode 100644 index 00000000000000..91163e2f7eb7ba --- /dev/null +++ b/docs/generated/packages/remix/generators/setup.json @@ -0,0 +1,23 @@ +{ + "name": "setup", + "implementation": "/packages/remix/src/generators/setup/setup.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixSetup", + "title": "", + "type": "object", + "properties": { + "packageManager": { + "type": "string", + "description": "The package manager to setup for", + "enum": ["yarn", "npm", "pnpm"] + } + }, + "presets": [] + }, + "description": "Setup a Remix in an existing workspace", + "hidden": true, + "aliases": [], + "path": "/packages/remix/src/generators/setup/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/storybook-configuration.json b/docs/generated/packages/remix/generators/storybook-configuration.json new file mode 100644 index 00000000000000..a379d7b903c0cd --- /dev/null +++ b/docs/generated/packages/remix/generators/storybook-configuration.json @@ -0,0 +1,93 @@ +{ + "name": "storybook-configuration", + "implementation": "/packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "NxRemixStorybookConfigure", + "title": "Remix Storybook Configuration", + "description": "Set up Storybook for a Remix library.", + "type": "object", + "properties": { + "project": { + "type": "string", + "aliases": ["name", "projectName"], + "description": "Project for which to generate Storybook configuration.", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "For which project do you want to generate Storybook configuration?", + "x-dropdown": "projects", + "x-priority": "important" + }, + "configureCypress": { + "type": "boolean", + "description": "Run the cypress-configure generator.", + "x-prompt": "Configure a cypress e2e app to run against the storybook instance?", + "default": true, + "x-priority": "important" + }, + "generateStories": { + "type": "boolean", + "description": "Automatically generate `*.stories.ts` files for components declared in this project?", + "x-prompt": "Automatically generate *.stories.ts files for components declared in this project?", + "default": true, + "x-priority": "important" + }, + "generateCypressSpecs": { + "type": "boolean", + "description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.", + "x-prompt": "Automatically generate test files in the Cypress E2E app generated by the cypress-configure generator?", + "default": true, + "x-priority": "important" + }, + "configureStaticServe": { + "type": "boolean", + "description": "Specifies whether to configure a static file server target for serving storybook. Helpful for speeding up CI build/test times.", + "x-prompt": "Configure a static file server for the storybook instance?", + "default": true, + "x-priority": "important" + }, + "cypressDirectory": { + "type": "string", + "description": "A directory where the Cypress project will be placed. Placed at the root by default." + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript story files rather than TypeScript story files.", + "default": false + }, + "tsConfiguration": { + "type": "boolean", + "description": "Configure your project with TypeScript. Generate main.ts and preview.ts files, instead of main.js and preview.js.", + "default": false + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint"], + "default": "eslint" + }, + "ignorePaths": { + "type": "array", + "description": "Paths to ignore when looking for components.", + "items": { "type": "string", "description": "Path to ignore." }, + "examples": [ + "**/**/src/**/not-stories/**", + "libs/my-lib/**/*.something.ts", + "**/**/src/**/*.other.*", + "libs/my-lib/src/not-stories/**,**/**/src/**/*.other.*,apps/my-app/**/*.something.ts" + ] + }, + "configureTestRunner": { + "type": "boolean", + "description": "Add a Storybook Test-Runner target." + } + }, + "required": ["name"], + "presets": [] + }, + "description": "Generates a Storybook configuration for a Remix application", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/storybook-configuration/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/remix/generators/style.json b/docs/generated/packages/remix/generators/style.json new file mode 100644 index 00000000000000..16e5752a6cabc0 --- /dev/null +++ b/docs/generated/packages/remix/generators/style.json @@ -0,0 +1,44 @@ +{ + "name": "style", + "implementation": "/packages/remix/src/generators/style/style.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixRouteStyle", + "title": "Add style import to a route", + "type": "object", + "examples": [ + { + "command": "g style --path='apps/demo/app/routes/path/to/page.tsx'", + "description": "Generate route at apps/demo/app/routes/path/to/page.tsx" + } + ], + "properties": { + "path": { + "type": "string", + "description": "Route path", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the styles in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "projectName" }, + "x-prompt": "What project is this route in?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + } + }, + "required": ["path"], + "presets": [] + }, + "description": "Generates a new stylesheet and adds it to an existing route", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/style/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index f7e296d9118db5..0face4fedf1fa8 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -605,6 +605,26 @@ - [component-story](/nx-api/react-native/generators/component-story) - [stories](/nx-api/react-native/generators/stories) - [upgrade-native](/nx-api/react-native/generators/upgrade-native) + - [remix](/nx-api/remix) + - [executors](/nx-api/remix/executors) + - [serve](/nx-api/remix/executors/serve) + - [build](/nx-api/remix/executors/build) + - [generators](/nx-api/remix/generators) + - [preset](/nx-api/remix/generators/preset) + - [setup](/nx-api/remix/generators/setup) + - [application](/nx-api/remix/generators/application) + - [cypress-component-configuration](/nx-api/remix/generators/cypress-component-configuration) + - [library](/nx-api/remix/generators/library) + - [route](/nx-api/remix/generators/route) + - [resource-route](/nx-api/remix/generators/resource-route) + - [action](/nx-api/remix/generators/action) + - [loader](/nx-api/remix/generators/loader) + - [style](/nx-api/remix/generators/style) + - [setup-tailwind](/nx-api/remix/generators/setup-tailwind) + - [storybook-configuration](/nx-api/remix/generators/storybook-configuration) + - [meta](/nx-api/remix/generators/meta) + - [error-boundary](/nx-api/remix/generators/error-boundary) + - [cypress](/nx-api/remix/generators/cypress) - [rollup](/nx-api/rollup) - [executors](/nx-api/rollup/executors) - [rollup](/nx-api/rollup/executors/rollup) diff --git a/e2e/remix/jest.config.ts b/e2e/remix/jest.config.ts new file mode 100644 index 00000000000000..e2d184e435ad24 --- /dev/null +++ b/e2e/remix/jest.config.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +export default { + displayName: 'e2e-remix', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + maxWorkers: 1, + globals: {}, + globalSetup: '../utils/global-setup.ts', + globalTeardown: '../utils/global-teardown.ts', +}; diff --git a/e2e/remix/project.json b/e2e/remix/project.json new file mode 100644 index 00000000000000..3a583364c8109f --- /dev/null +++ b/e2e/remix/project.json @@ -0,0 +1,10 @@ +{ + "name": "e2e-remix", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/remix", + "projectType": "application", + "targets": { + "e2e": {} + }, + "implicitDependencies": ["remix"] +} diff --git a/e2e/remix/tests/nx-remix.test.ts b/e2e/remix/tests/nx-remix.test.ts new file mode 100644 index 00000000000000..0f763f2478a42b --- /dev/null +++ b/e2e/remix/tests/nx-remix.test.ts @@ -0,0 +1,175 @@ +import { + cleanupProject, + killPorts, + newProject, + runCLI, + checkFilesExist, + readJson, + uniq, + updateFile, + runCommandAsync, +} from '@nx/e2e/utils'; + +describe('remix e2e', () => { + let proj: string; + + beforeAll(() => { + proj = newProject(); + }); + + afterAll(() => { + killPorts(); + cleanupProject(); + }); + + it('should create a standalone remix app', async () => { + const appName = uniq('remix'); + runCLI(`generate @nx/remix:preset --name ${appName} --verbose`); + + // Can import using ~ alias like a normal Remix setup. + updateFile(`app/foo.ts`, `export const foo = 'foo';`); + updateFile( + `app/routes/index.tsx`, + ` + import { foo } from '~/foo'; + export default function Index() { + return ( +

{foo}

+ ); + } + ` + ); + + const result = runCLI(`build ${appName}`); + expect(result).toContain('Successfully ran target build'); + }, 120_000); + + it('should create app', async () => { + const plugin = uniq('remix'); + runCLI(`generate @nx/remix:app ${plugin}`); + + const buildResult = runCLI(`build ${plugin}`); + expect(buildResult).toContain('Successfully ran target build'); + + const testResult = runCLI(`test ${plugin}`); + expect(testResult).toContain('Successfully ran target test'); + }, 120000); + + describe('--directory', () => { + it('should create src in the specified directory --projectNameAndRootFormat=derived', async () => { + const plugin = uniq('remix'); + const appName = `sub-${plugin}`; + runCLI( + `generate @nx/remix:app ${plugin} --directory=sub --projectNameAndRootFormat=derived --rootProject=false` + ); + const project = readJson(`sub/${plugin}/project.json`); + expect(project.targets.build.options.outputPath).toEqual( + `dist/sub/${plugin}` + ); + + const result = runCLI(`build ${appName}`); + expect(result).toContain('Successfully ran target build'); + }, 120000); + + it('should create src in the specified directory --projectNameAndRootFormat=as-provided', async () => { + const plugin = uniq('remix'); + runCLI( + `generate @nx/remix:app ${plugin} --directory=subdir --projectNameAndRootFormat=as-provided --rootProject=false` + ); + const project = readJson(`subdir/project.json`); + expect(project.targets.build.options.outputPath).toEqual(`dist/subdir`); + + const result = runCLI(`build ${plugin}`); + expect(result).toContain('Successfully ran target build'); + }, 120000); + }); + + describe('--tags', () => { + it('should add tags to the project', async () => { + const plugin = uniq('remix'); + runCLI(`generate @nx/remix:app ${plugin} --tags e2etag,e2ePackage`); + const project = readJson(`${plugin}/project.json`); + expect(project.tags).toEqual(['e2etag', 'e2ePackage']); + }, 120000); + }); + + describe('--js', () => { + it('should create js app and build correctly', async () => { + const plugin = uniq('remix'); + runCLI(`generate @nx/remix:app ${plugin} --js=true`); + + const result = runCLI(`build ${plugin}`); + expect(result).toContain('Successfully ran target build'); + }, 120000); + }); + + describe('--unitTestRunner', () => { + it('should generate a library with vitest and test correctly', async () => { + const plugin = uniq('remix'); + runCLI(`generate @nx/remix:library ${plugin} --unitTestRunner=vitest`); + + const result = runCLI(`test ${plugin}`); + expect(result).toContain(`Successfully ran target test`); + }, 120_000); + }); + + describe('error checking', () => { + const plugin = uniq('remix'); + + beforeAll(async () => { + runCLI(`generate @nx/remix:app ${plugin} --tags e2etag,e2ePackage`); + }, 120000); + + it('should check for un-escaped dollar signs in routes', async () => { + await expect(async () => + runCLI( + `generate @nx/remix:route --project ${plugin} --path my.route.$withParams.tsx` + ) + ).rejects.toThrow(); + + runCLI( + `generate @nx/remix:route --project ${plugin} --path my.route.\\$withParams.tsx` + ); + + expect(() => + checkFilesExist(`${plugin}/app/routes/my.route.$withParams.tsx`) + ).not.toThrow(); + }, 120000); + + it('should pass un-escaped dollar signs in routes with skipChecks flag', async () => { + await runCommandAsync( + `someWeirdUseCase=route-segment && yarn nx generate @nx/remix:route --project ${plugin} --path my.route.$someWeirdUseCase.tsx --force` + ); + + expect(() => + checkFilesExist(`${plugin}/app/routes/my.route.route-segment.tsx`) + ).not.toThrow(); + }, 120000); + + it('should check for un-escaped dollar signs in resource routes', async () => { + await expect(async () => + runCLI( + `generate @nx/remix:resource-route --project ${plugin} --path my.route.$withParams.ts` + ) + ).rejects.toThrow(); + + runCLI( + `generate @nx/remix:resource-route --project ${plugin} --path my.route.\\$withParams.ts` + ); + + expect(() => + checkFilesExist(`${plugin}/app/routes/my.route.$withParams.ts`) + ).not.toThrow(); + }, 120000); + + it('should pass un-escaped dollar signs in resource routes with skipChecks flag', async () => { + await runCommandAsync( + `someWeirdUseCase=route-segment && yarn nx generate @nx/remix:resource-route --project ${plugin} --path my.route.$someWeirdUseCase.ts --force` + ); + + expect(() => + checkFilesExist(`${plugin}/app/routes/my.route.route-segment.ts`) + ).not.toThrow(); + }, 120000); + }); +}); diff --git a/e2e/remix/tsconfig.json b/e2e/remix/tsconfig.json new file mode 100644 index 00000000000000..6d5abf84832009 --- /dev/null +++ b/e2e/remix/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/e2e/remix/tsconfig.spec.json b/e2e/remix/tsconfig.spec.json new file mode 100644 index 00000000000000..1a24bfb0a13536 --- /dev/null +++ b/e2e/remix/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "**/*.d.ts", + "jest.config.ts" + ] +} diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index ba434e9fbd8ba1..674bf6353d1fe9 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -86,6 +86,7 @@ export function newProject({ `@nx/playwright`, `@nx/rollup`, `@nx/react`, + `@nx/remix`, `@nx/storybook`, `@nx/vue`, `@nx/vite`, diff --git a/package.json b/package.json index e8f11a74b9071c..f5fc0c1e87a1f8 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,8 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@pnpm/lockfile-types": "^5.0.0", "@reduxjs/toolkit": "1.9.0", + "@remix-run/dev": "^2.3.0", + "@remix-run/node": "^2.3.0", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-image": "^2.1.0", diff --git a/packages-legacy/remix/README.md b/packages-legacy/remix/README.md new file mode 100644 index 00000000000000..8178d2dd5f2489 --- /dev/null +++ b/packages-legacy/remix/README.md @@ -0,0 +1,11 @@ +## @nrwl/remix has been deprecated! + +@nrwl/remix has been deprecated in favor of [@nx/remix](https://www.npmjs.com/package/@nx/remix). Please use that instead. + +@nrwl/remix will no longer be published in Nx v17. + +

Nx - Smart, Fast and Extensible Build System

+ +# Nx: Smart, Fast and Extensible Build System + +Nx is a next generation build system with first class monorepo support and powerful integrations. diff --git a/packages-legacy/remix/generators.json b/packages-legacy/remix/generators.json new file mode 100644 index 00000000000000..aa164af1557a2b --- /dev/null +++ b/packages-legacy/remix/generators.json @@ -0,0 +1,4 @@ +{ + "extends": ["@nx/remix"], + "schematics": {} +} diff --git a/packages-legacy/remix/index.ts b/packages-legacy/remix/index.ts new file mode 100644 index 00000000000000..17f455709723aa --- /dev/null +++ b/packages-legacy/remix/index.ts @@ -0,0 +1 @@ +export * from '@nx/remix'; diff --git a/packages-legacy/remix/package.json b/packages-legacy/remix/package.json new file mode 100644 index 00000000000000..b44d06c6754117 --- /dev/null +++ b/packages-legacy/remix/package.json @@ -0,0 +1,35 @@ +{ + "name": "@nrwl/remix", + "version": "0.0.1", + "description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.", + "repository": { + "type": "git", + "url": "https://github.com/nrwl/nx.git", + "directory": "packages-legacy/remix" + }, + "keywords": [ + "Monorepo", + "Remix", + "React", + "Web", + "CLI" + ], + "author": "Victor Savkin", + "license": "MIT", + "bugs": { + "url": "https://github.com/nrwl/nx/issues" + }, + "homepage": "https://nx.dev", + "main": "index.js", + "typings": "./index.d.ts", + "generators": "./generators.json", + "dependencies": { + "@nx/remix": "file:../../packages/remix" + }, + "nx-migrations": { + "migrations": "@nx/remix/migrations.json" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages-legacy/remix/project.json b/packages-legacy/remix/project.json new file mode 100644 index 00000000000000..ac8cfb00b63f8e --- /dev/null +++ b/packages-legacy/remix/project.json @@ -0,0 +1,38 @@ +{ + "name": "remix-legacy", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages-legacy/remix", + "projectType": "library", + "targets": { + "build": { + "outputs": ["{workspaceRoot}/build/packages/{projectName}/README.md"], + "command": "node ./scripts/copy-readme.js react-legacy" + }, + "build-base": { + "executor": "@nrwl/js:tsc", + "dependsOn": ["^build"], + "options": { + "main": "packages-legacy/remix/index.ts", + "tsConfig": "packages-legacy/remix/tsconfig.json", + "outputPath": "build/packages/remix-legacy", + "updateBuildableProjectDepsInPackageJson": false, + "assets": [ + "packages-legacy/remix/*.md", + { + "input": "packages-legacy/remix", + "glob": "**/*.json", + "ignore": ["**/tsconfig*.json", "project.json"], + "output": "/" + }, + { + "input": "packages-legacy/remix", + "glob": "**/*.d.ts", + "output": "/" + }, + "LICENSE" + ] + } + } + }, + "tags": [] +} diff --git a/packages-legacy/remix/tsconfig.json b/packages-legacy/remix/tsconfig.json new file mode 100644 index 00000000000000..9cf8a29c54160b --- /dev/null +++ b/packages-legacy/remix/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "declaration": true + }, + "include": ["**/*.ts"], + "files": ["index.ts"] +} diff --git a/packages/nx/package.json b/packages/nx/package.json index 7779f36a7cf56a..2a5e2f4b6dc5b7 100644 --- a/packages/nx/package.json +++ b/packages/nx/package.json @@ -134,6 +134,8 @@ "@nrwl/react-native", "@nx/rollup", "@nrwl/rollup", + "@nx/remix", + "@nrwl/remix", "@nx/storybook", "@nrwl/storybook", "@nrwl/tao", diff --git a/packages/nx/src/utils/plugins/core-plugins.ts b/packages/nx/src/utils/plugins/core-plugins.ts index c545b7685483d5..7bb395d4d95161 100644 --- a/packages/nx/src/utils/plugins/core-plugins.ts +++ b/packages/nx/src/utils/plugins/core-plugins.ts @@ -68,6 +68,10 @@ export function fetchCorePlugins(): CorePlugin[] { name: '@nx/react-native', capabilities: 'executors,generators', }, + { + name: '@nx/remix', + capabilities: 'executors,generators', + }, { name: '@nx/rollup', capabilities: 'executors,generators', diff --git a/packages/remix/.eslintrc.json b/packages/remix/.eslintrc.json new file mode 100644 index 00000000000000..d5c9b55048fbc9 --- /dev/null +++ b/packages/remix/.eslintrc.json @@ -0,0 +1,46 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + "@typescript-eslint/no-var-requires": "off" + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["./package.json", "./generators.json", "./executors.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/nx-plugin-checks": "error" + } + }, + { + "files": ["./package.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "buildTargets": ["build-base"], + "ignoredDependencies": [ + "nx", + "typescript", + "@nx/cypress", + "@nx/playwright", + "@nx/storybook" + ] + } + ] + } + } + ] +} diff --git a/packages/remix/README.md b/packages/remix/README.md new file mode 100644 index 00000000000000..302b159c501312 --- /dev/null +++ b/packages/remix/README.md @@ -0,0 +1,120 @@ +

Nx - Smart, Fast and Extensible Build System

+ +Next generation full stack framework and build system together. Build better websites with [Remix](https://remix.run/) and [Nx](https://nx.dev). + +Nx makes supercharges your builds, and the optional [Nx Cloud](https://nx.app) provide out-of-the-box distributed caching, distributed task execution, and valuable workspace insights. + +## Creating new Remix workspace + +Use `--preset=@nx/remix` when creating new workspace. + +e.g. + +```bash +npx create-nx-workspace@latest acme \ +--preset=@nx/remix \ +--project=demo +``` + +Now, you can go into the `acme` folder and start development. + +```bash +cd acme +npx nx dev demo +``` + +**Note:** This command runs the `dev` script in `apps/demo/package.json`. + +Start the production server with one command. + +```bash +npx nx start demo +``` + +**Note:** This will run the build before starting (as defined in `nx.json`). + +## Existing workspaces + +You can add Remix to any existing Nx workspace. + +First, install the plugin: + +```bash +npm install --save-dev @nx/remix + +# Or with yarn +yarn add -D @nx/remix +``` + +Then, run the setup generator: + +```bash +npx nx g @nx/remix:setup +``` + +You can then add your first app and run it: + +```bash +npx nx g @nx/remix:app demo +``` + +## Adding new routes + +Add a new route with one command. + +```bash +npx nx g route + +# e.g. +npx nx g route apps/demo/app/routesfoo/bar +``` + +Browse to `http://localhost:3000/foo/bar` to see the new route. + +## Workspace libraries + +The Remix setup leverages npm/yarn/pnpm workspaces and Nx buildable libraries. + +```bash +npx nx g @nx/remix:lib mylib +``` + +Import the new library in your app. + +```typescript jsx +// apps/demo/app/root.tsx +import { Mylib } from '@acme/mylib'; + +// ... + +export default function App() { + return ( + + + + + + + ); +} +``` + +Now, run the dev server again to see the new library in action. + +```bash +npx nx dev demo +``` + +**Note:** You must restart the server if you make any changes to your library. Luckily, with Nx cache this operation should be super fast. + +## Contributing + +### Running unit tests + +Run `nx test demo` to execute the unit tests via [Jest](https://jestjs.io). + +### Publishing + +```bash +nx publish demo --ver=[version] +``` diff --git a/packages/remix/executors.json b/packages/remix/executors.json new file mode 100644 index 00000000000000..3774ea925135df --- /dev/null +++ b/packages/remix/executors.json @@ -0,0 +1,14 @@ +{ + "executors": { + "serve": { + "implementation": "./src/executors/serve/serve.impl", + "schema": "./src/executors/serve/schema.json", + "description": "Serve a Remix application." + }, + "build": { + "implementation": "./src/executors/build/build.impl", + "schema": "./src/executors/build/schema.json", + "description": "Build a Remix application." + } + } +} diff --git a/packages/remix/generators.json b/packages/remix/generators.json new file mode 100644 index 00000000000000..6e500205be92eb --- /dev/null +++ b/packages/remix/generators.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/schema", + "name": "NxRemix", + "version": "0.0.1", + "extends": ["@nx/react"], + "generators": { + "preset": { + "implementation": "./src/generators/preset/preset.impl", + "schema": "./src/generators/preset/schema.json", + "description": "Generate a new Remix workspace", + "hidden": true + }, + "setup": { + "implementation": "./src/generators/setup/setup.impl", + "schema": "./src/generators/setup/schema.json", + "description": "Setup a Remix in an existing workspace", + "hidden": true + }, + "application": { + "implementation": "./src/generators/application/application.impl", + "schema": "./src/generators/application/schema.json", + "description": "Generate a new Remix application", + "aliases": ["app"], + "x-type": "application" + }, + "cypress-component-configuration": { + "implementation": "./src/generators/cypress-component-configuration/cypress-component-configuration.impl", + "schema": "./src/generators/cypress-component-configuration/schema.json", + "description": "Generate a Cypress Component Testing configuration for a Remix project" + }, + "library": { + "implementation": "./src/generators/library/library.impl", + "schema": "./src/generators/library/schema.json", + "description": "Generate a new library", + "aliases": ["lib"], + "x-type": "library" + }, + "route": { + "implementation": "./src/generators/route/route.impl", + "schema": "./src/generators/route/schema.json", + "description": "Generate a new route" + }, + "resource-route": { + "implementation": "./src/generators/resource-route/resource-route.impl", + "schema": "./src/generators/resource-route/schema.json", + "description": "Generate a new resource route" + }, + "action": { + "implementation": "./src/generators/action/action.impl", + "schema": "./src/generators/action/schema.json", + "description": "Add an action function to an existing route" + }, + "loader": { + "implementation": "./src/generators/loader/loader.impl", + "schema": "./src/generators/loader/schema.json", + "description": "Add a loader function to an existing route" + }, + "style": { + "implementation": "./src/generators/style/style.impl", + "schema": "./src/generators/style/schema.json", + "description": "Generates a new stylesheet and adds it to an existing route" + }, + "setup-tailwind": { + "implementation": "./src/generators/setup-tailwind/setup-tailwind.impl", + "schema": "./src/generators/setup-tailwind/schema.json", + "description": "Generates a TailwindCSS configuration for the Remix application" + }, + "storybook-configuration": { + "implementation": "./src/generators/storybook-configuration/storybook-configuration.impl", + "schema": "./src/generators/storybook-configuration/schema.json", + "description": "Generates a Storybook configuration for a Remix application" + }, + "meta": { + "implementation": "./src/generators/meta/meta.impl", + "schema": "./src/generators/meta/schema.json", + "description": "Add a meta function to an existing route" + }, + "error-boundary": { + "implementation": "./src/generators/error-boundary/error-boundary.impl", + "schema": "./src/generators/error-boundary/schema.json", + "description": "Add an ErrorBoundary to an existing route" + }, + "cypress": { + "implementation": "./src/generators/cypress/cypress.impl", + "schema": "./src/generators/cypress/schema.json", + "description": "Generate a project for testing Remix apps using Cypress" + } + } +} diff --git a/packages/remix/index.ts b/packages/remix/index.ts new file mode 100644 index 00000000000000..5b1c425f47346d --- /dev/null +++ b/packages/remix/index.ts @@ -0,0 +1,15 @@ +export * from './src/generators/action/action.impl'; +export * from './src/generators/application/application.impl'; +export * from './src/generators/cypress-component-configuration/cypress-component-configuration.impl'; +export * from './src/generators/cypress/cypress.impl'; +export * from './src/generators/error-boundary/error-boundary.impl'; +export * from './src/generators/library/library.impl'; +export * from './src/generators/loader/loader.impl'; +export * from './src/generators/meta/meta.impl'; +export * from './src/generators/preset/preset.impl'; +export * from './src/generators/resource-route/resource-route.impl'; +export * from './src/generators/route/route.impl'; +export * from './src/generators/setup-tailwind/setup-tailwind.impl'; +export * from './src/generators/storybook-configuration/storybook-configuration.impl'; +export * from './src/generators/style/style.impl'; +export { createWatchPaths } from './src/utils/create-watch-paths'; diff --git a/packages/remix/jest.config.ts b/packages/remix/jest.config.ts new file mode 100644 index 00000000000000..7d88d213ae4f0e --- /dev/null +++ b/packages/remix/jest.config.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +export default { + displayName: 'remix', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + }, + ], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/packages/remix', +}; diff --git a/packages/remix/migrations.json b/packages/remix/migrations.json new file mode 100644 index 00000000000000..7fa3dc455b7b20 --- /dev/null +++ b/packages/remix/migrations.json @@ -0,0 +1,54 @@ +{ + "generators": {}, + "packageJsonUpdates": { + "17.2.0": { + "version": "17.2.0-beta.16", + "packages": { + "@remix-run/node": { + "version": "^2.3.0", + "alwaysAddToPackageJson": true + }, + "@remix-run/react": { + "version": "^2.3.0", + "alwaysAddToPackageJson": true + }, + "@remix-run/serve": { + "version": "^2.3.0", + "alwaysAddToPackageJson": true + }, + "@remix-run/dev": { + "version": "^2.3.0", + "alwaysAddToPackageJson": true + }, + "@remix-run/css-bundle": { + "version": "^2.3.0", + "alwaysAddToPackageJson": true + }, + "@remix-run/eslint-config": { + "version": "^2.3.0", + "alwaysAddToPackageJson": true + }, + "isbot": { + "version": "^3.6.8", + "alwaysAddToPackageJson": true + }, + "eslint": { + "version": "^8.38.0", + "alwaysAddToPackageJson": true + }, + "@testing-library/react": { + "version": "^14.1.2", + "alwaysAddToPackageJson": false + }, + "@testing-library/jest-dom": { + "version": "^6.1.4", + "alwaysAddToPackageJson": false + }, + "@testing-library/user-event": { + "version": "^14.5.1", + "alwaysAddToPackageJson": false + } + } + } + } +} diff --git a/packages/remix/nx-remix.png b/packages/remix/nx-remix.png new file mode 100644 index 0000000000000000000000000000000000000000..9f873e22dc7a7bee69e296aa7e3b6b7995cea145 GIT binary patch literal 205679 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
q{qPjKL0#1M4;0)Ja23Px}$La@c{@y9Tz0sGK zdMe^|7^#4}QOAtS$Wo$2X9?n(0gG*7B`oLV_baS?RX>_zn(58savRLB6fh zUFLnw^*LwfPw#$<7Ivu$KPL>`F`lywzDwyP{^6TGbK=G1bkA6l&@%?e;sfGfxA3|1 zyLbAD&0Xf;>mEvXs?#U}$;eAztIMU2L>UGGqGE?y{tr=a85ULhy@5)DDBT?r(%mp1 zAgF|Z(v6hF&<#UM2_xMgQYzie&<#V$&cx}UY~ ztz#ZV@`dCxhv%_G0ldZzV25?&O-uVL?jQ7QW%_3b?;n2UA%C^z-OuOUQ^~X&Q9^FA zAifB0$QidR8e}nZHE$v4loE7$qyoB7SA%(4Eu|8tG)@0G7=ucF5qrd}@M1(r-?l+=KqHxLuvF7>JnzR6>Be&bUv zlx@o>R2`R|E)KEYy>iA2fSKKGX{~!+s|K(J&s`4?)1WMRRWSQe=Y*)eWz@Om!@^J7L21gBcaHG$=7cyfen)dPA9}rWtYEtWR+Wb)kHAO~_xYO$Kg%r~b zyp1oI>v-*Q;M!kJ(OP&(+287yfM3~UNa^=8_Dx_tS^M3VnKf8AjFt z4-({SYN=smpZW6AvlwJ@zwj`QN*pR&T^u6Y{4ev<{$9h(58)q(wiYvG{NNo0(v48!VelwuhpS*H3I@~iV>Ju2cm_tl< z>o>WWXZC_{^#CvK*_&*aw#`_|$4=};Bo|AfinNWXQfaY?Q$Tl}Eb!I4!F{>;uL+u_ zG8__{pCWo2EP8th6Uc-+m~Fbk>Dy`&bM2AB#>IuoIoiunkEG}qI>W^8NrDOva(xn4A%V6~5%N~n zxJN?E_`E`*4zvHKj6vL&If-LwXP}pVCEi_WUVtwm4K)RsMG)TOpstb{9@CQlGMD5V#H zIj2dsc;`6M{k6Bf1p+o!Oj%vet{ic5t}SV!UAzCoe0l#cpY7es>`506qesNXzX-k( zxcAP?54dnK?L|jYI2%gw@zj)wFLSjCX$I-09>- zUbCf+1Z!N+vog8)zud4$4fFKeC{xS!AlTwg3P{GOK!;5_s@EpVD&NKqSB6d|k3qxPAb{Sfz;4MBs~aIu`d_DUVlrQ67Ph?o z*Nen&z}1}VPovlS&nXRtaB=$Cw-j&%R8J%2(k}%?qT&T`h3~4}NHm_seFc5;^tW;} z`t=y=LmK5DTfKPY12y;{s9Ol?$Tn!l_9*Bc2D+CNy|r@K=K%ON2Oe3u|7Lykv`vn0 z(Fnl=)1(vK8V;~O)UoEbiW}@V|4vFyC%Xec<)^#4T$RNSO-WtCLLNd)TPW|ZT1##* zT*W9cUj((!&AzX4kytnQ7!%!}Z4-RI2AMvpayxAVpg^`*n}C+vO~Ac20bY0~9c^>x z{_j5A2rYcp3+AhB%czf$Q8CSIEv2MG&AURDRq8r-Z=QL7*de(9uz2(0=1l$4LagOo1*) zM*XmFX<+m1qXgj8#sHSUg0E+PF;RuB! zbTd9(>MhIL?PKCbYbJV2Gl22#DK~*z**4YYjTW7mLG+-j7xc682Zi{dKp9=vub!fi zq5Vn~cRLobertY~)1<*oZi2PK3;A&{4qG0IYG#U@rqG>7Z@6&bLlxm&VZS=`S^KuydJOz>p5*B2bru<=GWwUaf6% zsIDM9Y|;*_8kOWIBZe6!t6A3dNp$rQ@~#~coA>W|ZDPt@{?c5&KjTN8#pzVt{tG&) z!8hjcaEGhGPl4#dKX+fBhID4UYT)# zfO*vBG1_*qhZ4Ucu(V=FBBElQ#TA~oo&r&0(3N{I>JP{R(TIv=Y`d)h?XO0i+)=tY zA)*fe2@&DkcW0#P{%v^|J8%o++)0PgEzWS;An$qa<|f}dazy!$3Fe{y8BsYxdVmg^ zz|sMF5f#f5uNV&{e!?*>m{~;UUUv0_k-2^E9w;~@m3EwFz$Zf_pE(LofVu zm0^pSq(@2~hiPlt1;xU2+FlT10@hxUajYk#OSn=Q2LtvLxfaJnG`QmRfsX)7??Ug`6FOq#_l1A-K`x&%AvGh@Y~Gu~0Z`Ra;t2F%q*9)0*w%$O5i) zED~(p-PWUird!1==K5xSz;cXN;A^mK#X6F-ap&1dUuujRs~&Zgrfif=)p4tgS)#Pn z?2vhYlpS6J`alIpRcy-cdri^Tnh(GVxBrmx5)B$=g)H&jZ{W`JoX{5>aP^0(d3R?H zKHR86D`9kM*Ie7Kj^pG#ei3*80J8P<;!9`xxm*L(qu}sj)5mn{h#W-KN-zyFrH0Q5 z?+9Kc>nMJ8z(C8h?r2KhKUWsXInzOhehnVs&Yh>cYRGH_ecwg|9V3o9?zZ_M<)1IO zABnZKES3p~knP6U-M5>~T(#qCQd_l&M)FYRqVyTk9scEc7>?Ji3D_!q41lwX| zztV-zU4;CdV`pS8DeWT)UV0RLvW18P{+_ji${Q6?P{=)K=^kiy9obCp8&4gCl}WoO z;cN;SJ{%`*@PD>LmATLKxN=|;xL&TW>%sOtQt%2>@Kb8P$sfUEcN(3R?dHp7>@8Qi z4Y0sFw@u+29UVTK%~(cY2EM-95STvKWi-VA1t!vT6Z0xBseZFMb-%T~X{q(E`4|mv zi4$!coNaY$(@||IJ0^q}+)JExoF<-dt5D(Fj8x@fV2`}-O_)r~1Mo1jbf_ZR_5A9Y@SzdicWb^xYr{k8=xiq#fD;UF!2L9U_xV3o>X<%coLEc9$97R0`}QXG zV)$p5+De8EOS-^%S>V+Y&2%(7UYmh^R8?)SlCygSFDWcFPo@IX&&Z?rvc5!Z#hGNy zYuD^H0^)^OMRl(>-M(uk&s@wfh`a7Qgf62ZR!C$U#W=v^u_pDZ=|(x#c^_lSUT2fX z)`CW{*p3b2Bwl%89U5DX2`kt_UB|1x;>x|cmJ@?Z96jx$n@i4!Lt*VJ8xkUi*1OZJ z^7QUC-1t0_5(+C8`p{wj&3rp&cV05;(d^Jkm8=}yva(28u(0f4w$!A@o-`KY+T7-x zOKq&oC^C8N>+c_9MlO>gQJSA3kE1AF(ZLUPdfgLxO7JtV_QP zN$kXhprf@OVM;P;A0pKfv#1i>;yvbWdt@qcr7T3oEYX6?z5L`QgsIo^e92zg9dmHK z_2Fy44au{|^B!QF{6kvHo3&SK(|HdU%l!9K{3ieXKyvLxI=Dr5_=8jleLg;VOG$W- zE^Ayy&k+BQ7^xcj^|IUkGy9A}XV+@$BokOW5!GZ>(tV65uCSoFWmaaZAy1MaXxldm zd6V1;zW)PVx?cd@h#x&9p-%i&M3huV-HC0XCCl#21V|PEp?5o6|_w zm*ZH!=Jzb5{TJ$8!Z2rlx#h_qj3dsEMr9?kDqJ-4xO3|&W@72#=9mrln>?&+JHqya zl9h-Cc)0gRXrdd9Cg8|D)*WWTt_JWe-iR2i`d)$?ry^x#G}lGS?)ZJ_MqQurKOLas z_|7|UfcK@rv@p|265uAu8*rfrxOisxne-7$WieHuTJpw^@Q|uBz>C~o8@0763x*i5 z#;c64rb?truMYTaxS%ui|^SMcyx2auS!~34bOI{6TnNiyoS@QKKg1^lfA^jb9 zUh!;mN$oUk)yx~k%ZY0Bzl_>4_)OZ9m-L%)_$~GYLsyw2R6e4=QWP^-T=>S%<@HKjY40# zi=62X4aGJa`sd!~-j038eBD-^ceAWiA*XlsF-GArFYprktk@PeVs6ha2W^n%HQg@3 z<-8F?nn7o?T63Vs3uKwA>hZ(70$WEsr)_GQIktqUmv7qXGl$@r<~1>~uot5#@<3I? zq}g8%N(1dN)g9W-_|a+sh%FFo(-a*@virFYG_=y%x0L%SBJei*n9jUqz@{HV7~_;k zn3OtvZ19vb8ixcTpj5*{V#Texi)_cx)pUo6F+M^M{cOnhW| zoNHpJxbo({y4S&^^nTJ#IO~a_w3ptac48B_pI@?8&J2i$84`C4>tsIpVICz;Ue8KT zA;IIKzn0?^Qu@65K1sGFDC2xze(O(dBB^p7m1$y8UF8o}wrt3d`A-!95Fl zW}<=+9htLo`S&}}0C$_MTsz^8kBj1x8d05OE`a{h3Fh^kv_h5BI@ znqcpI8z%O8KS<#X$Zaxa{w-+$^)=>mSM$m+9=Lfdre1hfR{|r2PXtz5`E^>y?W$xT zLL9Ulpkk@eb*ab`M2CEM_<8 zq+Ps$_^1ivIO@}n<(qw{#hs(kb94FL`9=c%edTU-vpLz(IM}*D^8dG9pF_l;MR$%3 zn@I<8iN#kRR!7SMzqM(}0Vk+9H>(%UWuLOrII`~cuLNOKF!#;39{4N1Q>`NUui4-MltZ9Ekq?qf32$Xyee3FI)MORW*QxI z_DH=>->#w_Y#Zo1d(b1X9=|5U-{s2%ebl9_Wm+qO*G}UNp7nJt#oN56eW!UXktk;u z#Hope??BuYnLn6vuE^hVe?Hen332I_p0Wsmj-jt!Mz01wDqh^JO1IxG^AqV`unv>T zA7K9oQ8quvpTu`&O6bgAtyc|Cpi3(Ui=AujICE&YB)l1gFaodqe-6r~yUM#WAm&I{ z>RFF65kUgb=0i(dn~W{$rYxq;%JH7M17sQao4?d-S09DSUR9?>h<7&DEJgajgxLPd z;!%s*I>Wm;M@aOWq}*`MqI4@idb3lVf0MT8-?r zwE&eu(ls4FM^>uWpuhzoz@}sl&7~7LR3oL$VSN?%|&G z$HPFTfWkA=kN_4i8|usZ2YmIv{cV%w{m#AaJ>3Gz{OMmhE<5|lWpoQxS;c)m;10aQ zWEeaXXaBZ)O4k+|t8E+_tLQ48SN{r4S@f*_X$~Tf#=58@RGan}#EWk6qnQ_(9!N92 zx$@(}YT;M}{eOD_w+&(aetqW{$l-rx1%(_V?mMJk-ZEcUg!G_$uc9NKBHOzqM=jc> zoJ7HklJPbKY{91hSLv6d1NmqhGvERMZelDY?b(!14Hko^Vb_{~fLTM(gGOFlt7`n? zQo$`lzyaH7kH?m%hrN4Da=^JROC#}Zd@mlIpYWs#geT}B{d`GnX|-H`Ela*YrJg~$ zX}8VV5~h)*G9PcQcUbFT!_vxr#VTt|)?+#IgRgx^c%>}XUMiRHf6*eo{>g2ANcFv| z5dg0`N!q5BJ*yl|L`R{I{Jp#IW#wX&n=8l^gtZhfZ3i2%a<$FV^&wM#%}+j$yeSnXc|And;I5pwVlN81A-bll4RRYy75bnQ4hy} zWtYu$)ql=MN8z(Vaq`8o3xXay-KufxMd@$rhZn(UurOZeQNOta%)FT?kr}ul>(CL{ z7ewqm1sk@<%xtU?( zJ&_+>6#{?X|NIwC{{?l+kW=M;kc;jL2s#S5!ZvR`tvohz)Ch`zc|s zYO$qr>RMp?E|0s%!4TV*Zbn%%j6d}>w7B6DP0Z#^RVQtACjnQ`$w)KIYbHypfKFU!3%wnF1pclP)KJq06$cvLeVtV%*lJH8wOT=g(#pX__B)M)Z6Sy_cePNny9 zr>kCC-by=FD_%dkPB;4I#}zy+LT1Q&3KyM+mE-%B>Dv`V2Y)iI3gq8Yv?=lmII|2_ z{)`y^af-i=bv2Gl6D4+fecTGg=|0E$b;Z>59iL8fcWFNyZ#;wz?rb$DH_)`yQBL3y z^3SvO8-f-3PzSB3<>^@dw%9k~jYJ&g5`aNVrqd2_GQ4Nl2-XSi3A?JvPOFgK6u_x` zKgZK&B7Wu$uY?Qm84{u8)V+=sP-drDVvjDF%xd(^1&5H}4qN27t%2Gt-4b6@=tTc3vJ2*!8+yEn84T}A%XSO3Oo zV&E8Q0@=R^Ttx%|G1MO=l>gb!I|_X{xX&DJu2K1BQ!*9Y-#5A85z8(AsCN}AF_w;- zOkOr3H2`)7)QNI=HP8@#83Y6n5BKfD6R+f0`~SaQ7VjV0uqeAD>gX!i1p6_B3W(}^Jpo44Qe)vQI-Y?xM zTqQ*rSGf`lJIp^n+V@%6_k<^o^H=&e(NSIognuY(|5=Hgdz3rfZblP{YISOaLAxQ& zrC)<+D?1ujxB(;XY&}{s|9PVW+C3I(+#kF-{W#W@w}yhKTh*XY+MC!0^Hw-8wXC|{)K?Fex0P?w6RMm3j?{uoinM& zO#Zl(1OJxtX70vma-=bEu`#FPVIT6~ls}T+`(H30Ko9hAkIqVpDrj1McYY{u>OhuD zzzufh{_Dv1yEY4t5z;sa+2D%ZyCWK1QXjQSab4j}pVAkb@{uTmu>*$lBC$GdtQe&N zVd5Ysv>ty#c9$l=!VBPLHg)UeU zWs5bo?iCv-w^nU)g$6XQ6|iZk{5aF)G@Et-yf|stGYr$aH-xkgNa>WfoDRj0zLv9I z6!KRUM>Aw(D>J<9ZykRwHj7w!_PtgUHGcndGmuvl)B{4e@A>=vFBkZf7uwu|}mw!a?u z^@(B9i}|D&OUG?-bU?}yJXY}+4P5fi_E)s=>CpNbXWD@LJNzBj8+*`)dBAOQ?Zeg7 z$;8e-IJ25aq0i8y;YYCLC1A!}Hol9=nW|HHKF%8)GQaa&DY~H*K=6w%KqBk-`X?Q> z#{=`4>z@ymfgvqUk6`75-C(U%?o_k{9JF<>_7;Tcv@9>Ps!@0ZZXo+`F<`(aP6m7` zMli}$Ab~}DJI;#{HF`|&yE9{u!SLk=-3!TN$+Wz}`q*s`1q;*>u%+7rT9p9Tr<1&s|1VbzLi{Sh<_Zk$M-dQ^w*? z?m0O3dYFEIkxZq>qf$OY1?5Wf*wt?Wt-{qxHTM2Xd2M67@7KPG9y7SNR?OCm*ICz5 z*IADTb@eO!mQij@w7b+>@a|6Em6W7?*|*bK1ltqGtZ( zgI3%j3x3A~iyDc~#ozr8wE61Giyu7xiJUF|T^HaQM-y~Sh+2nIqvZedoJTNW9TI2` zv8l<1I!q+HYW5+}GzveuzQ25hwiW!h`6;nTk*nKwY}jWe*Ph7ZwT0AaF6Ik2*+=IP zf5gt~fSdagZ^#L{LdE6e&Sg&YiWJYpE@9OHZ4CDAbw{t<%F>vku@tMl^0Zy+Q@*GB zSMQvJ-4Pb!+-H`pT3~Le6ed6gbML1$n(L*5*<+FS`q0D*KE4f4|X|CIIhrJ3H<$3 z;_U4g?mxdm0SLQ0?3Zjv9f#ecx6ii!Ki5`*w>1Y zytv!N!b5w-HohVoSy}a(L+*=|vQ@{CmdGZYi$5eSSPu7%X0!cQ!9+)R?B`d~nAC%H zjU`*@bp_dr^6^K9XLSSp;x>V#gNHir9Gh;qo$B{3k4_h_yJndjnyYgs{;O|J9#{R8 zBu@pX!sxw%E>36q?=&H2$7sKyT{C1pPLjFI+MoyV+sa$}_f+DJu-B2a;tOg?2w3XL~Wm_kKdGtpdxp zV1h0P0mO#OZ7+o;Xbaad57%=bIIgnwIC_s>#Qmgu8AR)1xS zos+4fmrHG5|0+Nxcs=nHh=)sCgot`M#0aI1rkGg%c#G2PCEy3H3nI5)<{kfq zMSR*|HvIE0RVqKNbr`l26o(VrejZ_YtlCZA;2!{G+6yT2SPn8-Ig|!g7&iY{c9Mp; z(6_k!ma?ii;e#>txEG(bkgN&iSwr7 z@$LMq74Twm63G(XLC^g7+CM~fRxwsUwTkGyjR!Q^!q$O&z z$bA;)-d!Z&S+e;}hj4WdkK7TPVL#WA@#;#U8kU!Tt9I}{Xatbsl*ji{H1F92g{*$0 z-j}MMvd6T+ZmS0Vnm~eAE_t+r_{Zy+t#8kKhC5kqo8D^bHRek2cm5 zjDP8)m;KXR`8MAP=V?-eIHz}&>uV~*bsV((t6%UAFH;C0|o9Z~58&UqKjZT83K9oF8mG*%xPFQ@|-*%y4c!H}_iL3$m? zKP%GQ8N-p$k<#z@Yr6^juo&K+ve&ku+u*3HqR;!+km&$hec9(PU#DeV|< zioJ4##ubx=92>E~@{D9j> zifGRPa^^anndy zd2DttiF`-PdtEo+4Y=(-(yq(eGR~Z)WkJa+#`t%8z?=3GO0zo>f>d|mQ@PbVefi1n z%#W1?L7y|@RL~7E1%O@s)2KXimy&x)9e#;{vj9f=KpJ{-gQmL#ERwj{{*?@T_h~dk-Pts3d_8QfRj2QJT|F0GR+7i7MTFDcJOZsfmMlc^~8$NKFSbz*p z$0?%vnbs$e@`r-FeW&#D_lzzKw?&ShoB<5ozn7hnUwpf1*0*!o<0 zw~-%57_tvR^>`z>-8-vVKLee=NwLp>F9LYiy4>mI=zk24TXVBv&m??yG*-s0pz1VN zgD4B%%y|3m>JjnCEEcWas|7+D4uUgGhFIk;Y=KwJ>)R*kS#kIbnm6q8yMpL4FxwVy zU8v!T6w^$=a(7MrtK8`?Z^D=D4O>69eC3^-47T`6pG6(^SKoG%(y_OLRD`az8BN2* zb#eM3^{aH6icmaHtu6cb$y8B=+7q+BX7&)}0^p?s3fSdQ6|97y$3UDr6An0U$Y zw1`S(@-GH$yafp78(!-eLPsbxyW)r8pU})+f*&F_{v1(_<{(ouKbsN z;kiU^Sd0V|^X7@MY^NtL&p`7gzfAUx?AWWfq^@^%B6Ft{cITLrYqIL=EvZin%a{7t z108B2D6?G1OT1Xo`(>Y>z1$!;!#ft*DKc5P-TB@uPIyd>TNr>^_pH)%A3N;z$9&Ri z|1+DLb9W%;&>e^85JC4{#GINTVBLUd!VBrcCGW#_eABQo^)gV;;C?fNX2Eyyg~<$Y zo8#8Y^xyWIIj-B)3}L4?vLg;0u{!M+f`{-g5ub0hKa(bLm$n%DST^9Br5EZ&quCGfQMQI;l>!w*$a_%LJ1usJ*6h+G2(|wMps&9UZaZk+YWcizTvtm zsV)(CjtNE$LQbjWsTlVGQ#+?`A4uzh^m&`4thZab*jZPnGQ80C zn$PIJ<;21k(52Euz;=7UMJd`^@I~f$ZNT3!x8p`0v_BS(fb-58!YaSH3Q4EF&`V*b z4Ygs0HQ#BMMI|=`hCTev(()de6BuL??eKj}Y($2FP@(5A=FgMkrSou-(_b?{d(oR+ zc4HHIei$jrz^Nb2f0Ap|7xESVtEX+?yfDK|5jBGj_IkGC;vxE`5n5&h+oSjh18m!l zB5$nJtI4S?JT7mJyJ)k5^ODUZE#k=B+~t-;wIk`?kg|r+DlN_x3+LkPgY6TUhJ&-} zHqSxbGf1d?+FGZY<~q9cbW|f_x3}-~x2%ntMM}*}sgY_Iulps&%(N7eTi`Kkg+2Kr?^dN@G;ZvK zpIV?PF~I2UbGK`65x?*V;3v^RTeKa>3-6#aTTRHx2xK^5OLqeUePOh3hdo5D`}63B z(3o+15E@d@J#kP;oU*yguKk!uB=oiX(){5-(qjS*IOF8FyHXmTnf--LEz#)SmDOA+ zzZbilI$Msd_WHq^%l<3OezyR(0u#D>cHO*Km=YQ z@B`Vil+Gl8jfOzYm_HsD^PS2{P`1svHo-#!0~Y7L^?lTHAE3>`K}}-|XXI;x&|@Ru zt^a++;xlx-)h2uQef`SznQUD+DQOBv(#@K_L#~0qd~=Vo4WRv0u_6xYMXUBVD^fOH z3^Z>%Bp>V6XL(BQjgXd7<$uoG!J*V)O5#}c_H1)F%lpqK(JSs(5+92Xo=e#mhukD; zf7tufpKPP8K;-t3(B$<7rbH4)7Q5rq7<5#sob~N{F((c}xW@jE@yOF%huHHrjbg$q zy*yUTOQ^7Y>!K*(Rlsn=*I0%277ypSh zWfh)~LMRidOQ+hBwr+*mvxUCm&RwXa1!I(=_0T(4bR#l5>HVK_GX-zHWRgx|!=352 z>OC_2YK3gDXRe;hZeO@ol=dT&+JlD5JFtBrazM?qj-I_TCojhPfuqtgSwFj9AsM9& z{-TwkRvv%5dp%!XFT)Ot=3?~Ttr2v-IIS{v?_MH%Ml=1KbR>~*o!!E15cEt)vjLin zwuskU6DK5-?0=PxpHeht631t+7|}-4*1&d5@U_pPi>tC^40?BI2)|TwdQdoI1*>@$-cKmVoa|vVN*&QJ zbO(t>tx+MplPlZh6FA}ax;xv)d|0v`C^w&1+lO)l;$YaBd+_RR0 zB(@N2!q5#L=}9BArk~%nIs%mhCTZ1LbpBBev-)ex$9_J@3eAAAJ;)s+C{fb+IBvfEuD_w(G4PNOsbhPW)CxmFs4>1~ME8bggSe+71qhaJ)f!qt>4gZajJ zV$4^w-SbI?_@yC->W1OjH`=icy*^J^B3Vz6s!dn%kJd`md!qM54p3xg`)3h>?c{pG zG*@dHUVmC^bCzSw!oTFBKce7DeJHH+RtiNt^XN$HMSmXI5N`>gC@Nl0&*yq!T6Me@ zl8<^b5r;oULyCj3JPF&St<8LW+^_-NjV!v8?`d4-UH`x9>RD zzD{hs%6F^K&kV+Eesx|cQ!Ly@(9UKKq3UTRvVPi@&j?mBH>XUor+q5v{AvV%i)K7LwyF$T00 zg}E)91tX&{EuwHcD26nq8*yH<6_b)!`$MolH9vgm8%ON@iOML!yuIR;Y}&zGia_N@ z*7HI1u-)$w2*ck(B+}Fy)+*?FZ&OVK!5MC$?g|A|X>N;N<}N2QSRR+HEVXSd1P{U0 z!Z$LTBLuKt7oifBn`y{Ffq=$cE41_|uOsu6lqzRFzu#8p!>9JSabiP_b91HwWXUc= zSq@u!=`%gH`@6J66_jD7!o~0C4k0HgTzBe9%TIp1TH%wie?CE_Dxzt+@a&tqix!`^ zI62)I%iqW^U%NI3{x-0K%T17W(vc&$p?gZdgF!?VAn z0V0iXpQvY*IpN}RPrR5;mtLcJoqCT3V2MWPg`K~#k;}%*y6J3uM&P#D@W?8Ka4aTI zPfl>x*9`Ni2CCMbY^(U*DAHYLRKyIhvci z*@1|;)+c?~V=mW^^*;G?__dw2&gJ{}mq_#;mZb2HW%AKWmZ5XSYWVM59nK}^8+l07 z0OU#}-c(q>!BjkkG!d6H&0B=0K7Q)|I6auiWXQ=p&l-X=2#N@R?%@Qj;mFZ0OnUvR zVjpvkb4Yf$>oX7S{w{-+5Dg`Q&;wVj26rNTR z*Oli?3p+ie2IB|3GS@#}5@YOwX=5_aRe?GGV@~h&9=nHR?va)LixWSw734IRmjo_m ze_WzRzLv5BZNR_rlDcFYsqgC+?6XLWi*0l4r!sWfTsN)N1{qrY-0HfXeNxm2HkSdVE#{Y+YuC1H@5Y0byizjz%aUI!oO;E~^QIR#e8Yf`S$zB$v7)bhy$Lu2IBIB;7A zF-{9jKgoIBIt!(xy_{>d2{bG;iX#*AatxCWs;^`}{7WWSe}7~zpIb4l^+IjGPxYWL z*z5Ht*!RiQg{)+OjW`XgNY1>lTqemB90LVpT8*R`n#&atyUwj59^rt29>cyx7IMjG z6P8^->{9viG?q{Zx3Dj)4)=`eW|02j+^I=$98+yF47?-FxA3-|B`uBcu>z{iZa5Xv zP3qkLuI?nFLRKjp`+cY*ojWVwIo)T2NrYQU&;{A+ksVP>j@Tb5!9yct>XO#cFMk?@#~1@q*23{F@~9O0heEp%=>M%)F^IEeLO7OT3XG*NK1~>Md%2 z>@nK?SE1aM#%w)h~qGLGUN%at?3UI`rIcdj!<|SJ!1j44=9=(>}&omKM zLFgZ9{+gp%8ISxfc-heEq)vimW^J%AYlzMut2sQ4dtB3Iw|XAnnof}aR>WsWDz~(a zC>Ss2NuC?wLnohuj)hN@6NCch-ZjRSH}Sz8xJ2a@u*2P-s7N)4{b#G$`u(a;jJGpw zW;(FN$1;sKjYl^z$1V7kXbsGJqwx}BX-p^ilb)q=s>zhdvsRU1oUnys0 zugpuq;ndx`{)Tt_!37tF|0Ou<{DON?-jn6k`{feGhjm7|`%&oKsKW*W?*ARj6;KRD zPf$m9Nf7E@@qP(eBgJuo&!bxc?%!md})fZFrx3L!#-&LPgkq>sl_I3>wwk zsTfrt56i8I5+kUTRATSgu(9r(m&(EWzAfd5tM=7|zI^tKcnXB#{r z+xG(E?AIN+%$C!dV-Gmf33~X^rGc2UEyF>qZKH<66vRcWlH|l>NTS}-&YTq)x9#v` z#Z6a*`4X2ISuNx{&t9O>n#wIg6Y=^lRr_Q6z64K_ok;95s}v0R-RN^J!n4i1Ki89gW5{=}T;jZ? zAYrNc;<={42~MzlP9bp-rt?HtYGwm{HsxKJHvIwuv#TmZUofp3#$3~HGsf2G$(mh5 zV?+ksv(r9SN>m>5v}MEqk{ML}O~I9^KVsJIcwr(0eoo2Y)EYh|TlO3{|d`fq{W}p!hEE6Jr_kEzBvZdtq^m*N^`E_*l z>u!fYnUiHoOKTjRhqf6n^4pH1YCXL1n&TJV$=dd>2l3 zU;UEX_*a5fv|c)OyJtUk%XWWWpP0D)#b&s4Il3$*I{`eB#f=mHDmV!8LLAiy3h>*5 zwL9%Du^wzJZMs&PQ@GFkWMnkE5)RY6R;|;4_#WJOj#n&U`IpepeG7A&?~q1yHJF^nFT#>E=}A~qxv%{-E$p+-%t$a^ zdtn93+zZF#mY29Rm{Xle)3$a`g$J;A7E)(?*k2g1#^Fu|jW3oVIE`DVu$n0B@-I z8);k{P5w-Zoel@qtR!hm;r!y2;EtqNJ2441AVN>yJ&!5mhT~61ZCJSf>|hC5bBq{Zm0MSGOAL^XVGX@7$jvoN39-!Yeu zGN}&PH0FxNbZ|)m>4Vj@aE8%yr}%Wt3rh8I1{smq&v1WtK<^nSp!4UC_6B)ErwN7) zD#s!O7iQDYh7Y80hqCntZ5ca6E;0+^@<*KU%9ZAvl*$ohKfkiOt%w_n#6$f~rQbu0EDPF1=Be9p^3@7Yy%n zBE!wD$?Y)W0+sOKi)KMrI7TvMo<`MRmCez`;_1US+vO$==bQW#bm6fZ14RU5CzO$H z;U~k)nt@0NXXtplErwoFqk~NHgvCCPfI!E~graa#?!p09{_>1~a{|*lr@Z58A%gWY zUnXwf^Biwu7yD1TEP|(#goDVUe=Sf^8`0xUi4fmi39sMSpAkv%sql7TV`eGk;U7 zwI}Zhsb~Y1RQ*UipG#xyGN)gHO<+rOi_eckBcJRva0u-BV-Pw|%|=4n_`GTAMX?;X z4Jz&XbL3*_dB_0DOkmmrwzf&_=&9YZl~6}vNyKkycur$yeZgT6X9#0F;ZAqjbj6Xl zMY7J^AQwN!3zfsINHo;dq?o1I8q$%-_47;@9`<4KSl2I&SoD?kr}QSF0Et~r!J7avJVJlxB~RtKA)<^5F~X&KocPTWlTV>9icwN4}SRHKn~ zvSq%R^HFRekkwqVVBhrk z`dVt3rRkm10>SbXwj&G=7O^1C_>|pGf7b>2Txp?uU5tf~EcXlHGM;9uO5Wh>ssPPw7-LU*m`*Kpa7&xQm94K1 zVn|Ni-uf66>|YdgGIXInyQi&9MCMrT&IUuL^Hqcpq=Zd(Ie@3-e07%z);^j<#7AdJ z1ApU*YpB&SF*4e|cgdZlc9A&z^#BU|2qXPk9M=KM^)$=))>qJyXiA?}b-V_kZ2 z#aa4Klz=9zel}$8Ytx%T#xd6tr_m6;1#M#|mm^;fN}JmYg0H?7OIoc_3ym)NnupE# z-H-L)Gk~TEd^PYi4#OUH0F>(jU8#89kryKX%l_Q1IGfP3wtxH} z(1w6<$5~X$MWC{bP!W168e=^xan+VJv9KM@V6s%QWBUtKT-DgW3_4v@2{Ff?MF1}8 z=IVE&6%MU+`wc5S%c|&$iy8kiA~-ubU8X%D;UP0&v~IhPz1lvCLwc&6Zv9t#0JP8e ze3DaxbM$~}@B1&wa`Q@v77+d{(n)vKeZQ?6=Uvl4&OuNQ?Wp69jc&M=9oJ9|Sgu^3QVFMV6^#WL)MEL2AdTR9=ab?oh)Nw0gzqJp} zEA9S;T%TewMAG8(li|NARF~~?rpkBoxPa>MdQRnKT!KE^oNG*y#%W>1IxFK(XU%={ zqm9Ur`62gEu0vQ4-X>G92doRG5&5L;2O((D{S7zmQX*Rqyy;f_w*kQXzaR-EuD=(U zEF4modJ%|)LJf-8mWzi7In>;qHKMO@A1>1*KU$qJH={3UYZAX%G_C42YPGmRZ8+^! zN)ew!xE8W)SeyM5^CpC41k?@};7YI}Kx^sK78#HpO*r%Tev9s^M7LOVsvwjD zBui0h%xtH9-NuquQjda}AD~!Ufcp2#ALZ`6RA&i;8pu`${O!5~3^{K8LoGrKH->*H zyd$$uas`M|g-HFjCUytZ$h88jI3ZlnVd<91U8^=1hs6jDwNI!+^J|RXsaEOT9w9Sk zt(EK1*q_t@(XZMZ6gd_mnPV{aum(;bG<@8=8VPv*w3({{>HSP$oKtfhm6_4(K&9hm zM`L0#p=6*0%0OBb3(Vm11Ecqul>~5e9-E5fA*@WLPiG|r8MF(lh!(}h>sFftQI`PK znz-24sMwLun*Lm~M%fq__6YC8n+G*_{|#hsz81lpfwO(Eh(YiUB0v7n+2WiabFqgk z+-_nOcW?o@4S#{adbK!0ttMR!GpJl#887Tse4RS2S_Hmf9fmXkYJ{LrfHcYryLI!; z$i?jf=H%}|q*WoecApEI8QEpEY@Z>{cFIfABN*AmLL+Qlovju#MHuu{ig?q?<+C*% z;0n2pNU-y0f6iOcNn$G6DE%@{_KY?R+0mw<8Ms`+3XjX_oRjp4(!p4G-w2eARpPYN zCSeq@m)RD*F!wUF$Gfly?Jk88y0BR;s=}94cJkxa@qU6S>)`rvom**tO@7{c%V>^( z>la(M+uQVS55&MGNMN|+)mnd3;3$AmwPBy&I9y|By#rK*XWsp%@ns+ew#9dtjMYA# zH#ymVw)h+gi!pAl@(iSmIa{poY#ANnWQd?$C<`I^@3qY_iif{^)Y|guJoJJFB>xW% z$9tK){(pN%!*HF=H=s%^ZqC<)yN?E|tY`QezTlM5#DpN^?-`iV=)wAU5>FCsa?%!6s7|#0} z9dI4h8#kxf02{zLv4wJ-mjxc2EW8-ZYA*|u_^9l$E5&5y(4aYr89}0ONWeZ)-Mo4x zL?(I(!-@>EqP{-JcAL&M`Ne}Z4_UQLiPlPJGUll9`9})FfxzleZCFl%V$L_VpzgQM zya<*aQ%obY3~BC5=NaxS3pvCGLBea;T)K{f&trJ5_RI|OFK`MBeQ3oMcGLYi4FJfu zs2#Of3-}2DtSndv*?E8d`zY{9+}cYh2I(barI;x?a^#x4zJhtF(0OAasif1yco|w+ z?qp`Yr%mn&uG;QOlxi0&IY)5ZstST^&n(O7 zjQ$K4fMb3M2(N}B;kVHp#eT)*2baW2dO8PVQAv%>4c67d2b$RF@Hct#Yj z3gt|Pq#YW5HsYAQqOt^^oWeeXP|hi z%=73VNkg2}0?E+E*glPr)jmdR9dh?@goB+-L%f28o~7m-f6K7Uo~v-YJn>zl2Qo=Z zR8hc3DGD8p+Ro~q`Wb#iA!7SwVzXl{=$4bF8#v~PU_b%V?bq{JPI-8R{dWP)HELj= zv~1f;*1{|{L5|o$6C69NPthadOcte)t{UUB8bX=G>@uI?i$r#Q36FyY@B|ngo%Fq) z{TM7a7(H1N;E<<|y^6WD{GU#;X&U@%DBI<}!R!8u> zzE*N_)KhpVAPruJ5;i)3^CqzZ@~2AeZy%D}Q0=0_R%tTQ6wD|9W=GteE|p3b_6wsj z=jZ!Cw?3dP@+?{tC#By@m~WzFuQaB6X6J_CwC~o(vzH4zjrm=C!htn0#^Yn!gqJTzXEDWkBT$&RqDsG`(6@zOviw{ zOR!g_R9ep1PV_g`te=dW)LJKs(JsgupRu&?&`CP5#YItMus(<+ZLm&1Y*)E&-)`S` z*+WmTQs12s+gRT6FCBO};#W_8hsVf%w{OPLlh54#6;X9OCRk!UoH*>i{r!d!6a90W zN9*CrQU9`JZ6|p+fu68_%fWD8Qt|{0EkQU(+AVjO6V1KGId=cbMNG zajieA*;Pi03k%;#aYYkW^PMmdry5}wCwT1Jgw4cOzY(eEd7K$8+0M!Q1!4(C_PpDz z`9_FqQG`TWKN_iaLy*>15iyY`n;YtXp3=(1NrPCyy3ZeTU7>H#1uos4(&NV8Fq7;9z+W}86Mb7=jmjLbUNBkGR1nO;OS99%Z?db7&;VxNAbh$=$%$>GScoUA%6lGb9U8^R-U z^k9VgfN8puW@M&WQbd?*-6;cUp*skai0O-@NK$JxE0;DnSl?gly#4|$uCiI`A^m=> zfz(}aFKFwMTmO*3d{5&D4qu2nKsVdYc}|fsRU~!lM(iW-3dL);CSTTWmwlqu^S};z zO0p1^|A`vEQsJKm*)Q2Quh~nepm|h)myWLc`wgLlpzHsQ99o8%9jt0cA$y3&;aZ)R zZGY`rGm+a_B>;10V_TXh}$YMOiC|XKaYc3tm>?3wia$lXj z*-np!jrv%bEAtTuF5-~{?i?yh#Joz`nA?t>N7evXS!znOfAr9J2kbNpTO3D`A|ppw zAlPmqv2*Qhy_H;FD(Y^8vXH-)af+dG51^EuhK z3Adcifi`gWeWfD0xPlQC|>ID33G^l z&pv@D)fBD;l77-#F?Wzy)ed?xr+ZHDXkeDEpF~d;xVNec_~fXR(Omq~uQ9FI6!UJ{ z-LP<=70Q2@)wY0mc@doRu@Qdd3ubg!!IrU4q{jl4#Lmzs^xJ&(Xa6Vh;1^;EJX7l7 z49aVBv7Q35h>h^tNQ(ZNt{D&GU_?E*Lxa%KM;M*968?IBjE?{TO7wmDPT;c8u>U*_ zk!Rta{2C%2QEq#pW_n8iqxGQnqfN@-p3t-!0^&*_DCYy}U)*U9?~IbDAJ*!^!X+Fb z%&YW>H_hC{NcNRV?UV(^Tpk*Fij5Wj&bcv$Fb$i)vCqRRM%t@+hGC{)?Cch?m<>d@ zG6_lM)w$JL@R;k@!}~vfWqaaU@mu^nSAoIKC`akW9an;Y+fgYp;Y;L87XJ?~QBKJ1 z#Uw4r_I~zz4oy3;0gITM;~Gk`PFQz!}qWt5|bkP1-RiPAW@wxhakJ z)6bv6?`D6qa0jZskam$wWyon41AtV*+J$ajX8mb;e$cWDo{nBo0R7CcrHt({_=;ny zk8fCy94jLLB2OX*sK?#czry`Ms;NS0vb_@si~5{`?N4F^{qUffGt7T_!6WmR{tBu0J}T(x$M zN%zNOy@a_OD*igU!yE z4{llg=<${y*5LsSc+^EF$7@JGL~RNjpx74guIS}E+Bfw7bR|<m=mc7cctF#MQ()pHcYvh?N)dQ_1PxI^;OMsuo6xu04Lf#$8a zv)jwZ9}bkR64m6?{eJ}vTs_&let2JxR2Q!R7s#O^xh2#@IcjU~G~B_zSOt#vvifJp zFqwcH>*5)x%b4VenQp>=EyNc{Gy?_gP?KZW?U)Y>EE~ zF!xj6~jf$o7NfAtLtILMt3Ha#p&jjgS-s79xEHJ;G+YQ`&(;^y?oZ6Gn z#{0CT`UtvCUdpp6VP;g(Wp8_8h1;95RAdc`Z7-n_kxoFIYO6wl*kM7x z!wE%-fDcN9>Ri+ACbX*@N{34U6*LQP!+3h z5%Sb$0}OTNX>}5Ari83Gja_xY{cn_&Jol0;kVE2t%?TdVz;|@44=l|iVhK!z9*=R+ zfT#Nq3e?@Q4$TiyG9Bj=EPv}@Nl>abqien`YhEsPRQUn`rcK+|IHKWGSg!@SA zan1-(Uu32qF0@QbKH~$`Y9W=}c^6VweS{)vZeH6$4Fw-sw#^>K<{2xBO$8b8WM;yw zeC|e{Y!K9QCOx?ELQc1}Y?B@Mv-1G4YXraQ<#ZqhXVNt&1azF-%Khg6`zfG__z7T0 zrqt&u9LkzXut zmKEF2bU3h1aHln&5JbUNt3QP+%GvVG4*v#PdZi zq84UCHVko()%mJiLwXu`;iikk3l2s*tr7(zsUgXUM<8{knr})WO&=LVedx<7knI3L zJLuG#OLF!8K-G1v!QrN#N0hJJDfh13_TzaAugC`a7}3ghYy|sL8ZfCWRe>oz$~y6r z7rF%xfD-dA+wDdJ{-_`1N}hE%Nno7mwG^o?|K#kCuzBLE(V0n08JJ9$FTA|PL?2PgkUl-G)BH~D!eWeAY_n4u35 zjxt^x^X4DHv3bZe9TzOAOi#+UFNtnQX}$P+#+4~e@%qc0w!=M0{#{J=^( z?$!UJ8>O%d*he@LJ*rN!N_+uz!aVWNm4JgE7TPA44``L)f%1={i?GHK$9V!K`~%9iV~xE#_3oNT`ROAx zyur~i&)ibi{cYCf<-Xlh_l|5A)GhOManl6TA|Y^E=xvfSA04T}?y*SW zFd8!Z`y7&QIwd8T+H6e~1xs*=F2~Oi3FETte0Zx~x=J1^^4x2OOqf;B=$7g< zB9yrwsb@p3)!tBmzBf5nHZ~RApCim6{!0T(LdeAg=yN9nbL;wj8uekE)?V?NM5-#v1oKTT$`X{lc5BiPyVhAR8g7BEv_osq*<`-y zmY)7I6bY>4UQtDjdc_{;Rv^Paam`I%OC9a)2C4P-Z-MqnGuOnI02t$^+kL}Ie8poW zkaXnnoS1TInEBl3U^Ynk8L>%8q-j$!Wr8M0OwZEZ$X2D##F{-VJ|`VQSMhrEre<4Ql}71HWe*gVb@6{0jxi;r64wo^t?%|6o)o{;S=H z?&`RAY$d-11*uJf(%&GjO0v)7wyVA(rORZfR-F^NtItWc2WE|*8xMqiCbNzG>QdcIeNN8zzOXeb&xz(2T5hQ5gg6W>acA-zr7Ku?*@nAzb<0L(K_bj z-_;Sk*^}%xx%z77#V^3kW|LP#iPM_`{^gC;t#;Z14Lv;2WDC8Em#Li`PYOb}(8hAY z?8(sNnc3hMs6}(_zt5uNNUIa0B!a>#;NqSnG&|%ERs}yrleHX283B&_;b<>Ylw6L& zr&;i`_==H8tvE}RCZC$%7?d2a1Eo20R-C%^Tn@8AjpIaqX)7^Y!itmRh>P0D#TXfN zU%L)#NW%-hTYT|I{>R>}*ceC1sk)BmEtU}DIa<)7v}&KeMz&SSFPI1c1lxoEN<073 zK^oYyc5Ps#^fCKrgBf1CoNYccc?xHKSMqGc9yc`Er$ab5aT#w~az*P39&Y-N)YRA? z*L6(zpyqd?lI6Q6>$`V*^A5qY16%}c|Ch{MNTOY`bmi3>dKj6#_&(pW{6b(1vm)JqjB)C#lGKD#Lbgu!*puPb zkHJRDM9et86{(E>;y&G-cMY8M*`&xi+lsOOsr)3Q_BD6@L{iPMcmVFhjS(sGL}0YV z3IT>YnnR0f1g7mJsVqB(IL+!Z!;8g5td;($P#|PCeAv#jZJ38=%+21Jp$QkM%Gr*y z32(n@)y*WrWU13&RxM@H4+q2X>~w+?teG44>UUe>yMMd;cB%P9eCy`jm(pPaA3^nU z`IJtDq_bd<5oJ@2C}4e#tvf%6_>$Q_-lAjMC;W;CC(5qYG8C}b^7^okavgxO{5Ym- z+HPIYEm61B@*B+#<~#NoGP0;0UxTeV-i9ob=6UmlPQ(aM&ohwfw;jpGO*Oi%_O zN+%8zv%4)$YY*8g&W-n{kXHazgg~I?Wn829uk)GjtG!dy@=b&z7o7T|#yMsyiF{A1 zhvc#IG+7?A*7R`H97MnEh|PkccKSfuK=L8OMG;BBpg$^4yjR%}js!AA!L~g4>Z#R= zgy>j(Otm&{I}EkK=%}_fkB-{~x3GA!X%#mNzP;fXX5$n_XOGv8O)D#@MIC|Uwpkie z3XCajcH*R~FmWuGy{TSSh{Yksg`~1xx^=1Y;!jriz74q44-;y~Hq?T|JLFcCsH&SF zwg{kY3}t4?_{TFY>q?(*D?(!?MMvlV`L|B>oL8oO?=7&8G%MlOgR~9bDUe| z(DN^sO0yp*(3sj9#C%WA$lDr|1V}S%+k~15Po;FgJ6eRDqw60AqDNV58{N>&n<9}8 zI}w8C&Xk7t&>b|UrVnt?Cm)whvcDoa(|mxc_S)X>9ys9z zp1}CB+MPfYBW{C>==h1whWxCn6KQd$Mk$vxj$Xvn$%js&dhy8cACDE0amAK+kuKO` zkHp)5T#N&ld9y&w@3h#^N({LIJ6I_i;31A`11Fv5Y#Tz4W#gC2y zC1%UD-z80*e=VXzrU1_N;ES-a1P$B7WMeNj+e}HB+NpKCG4uBLf+1}PW;p$Cd)={0*91l zne9*<3tzNbvNnS*Gex^!97p*PT6@S`)p+81-c_Lqj@P2%Dw=VRwIb2FRva(ljve*i zp8XOwMHZS(Gs__o*;d}C+dX|1XY2D;2+L>(Gd?unV(pgJvqfz-J=R6_bnNE~J zZhxb2?mGwdO92B~N)V%y>^})1=U)m7G-I-m{ts15b{Bc4}#klarhnl73%+aViN9c?*4U)joSV(9j zBG1+ihH8ujqGP=da7?s1(%Kh&&cel_$(pLSL0I^Vl~v<`x|_EnamZJv05Xs>@yGfA zAfr~kvbGlXD_(>qSjUaXe5#H@D@?Ie2`QYR1lEUQ?(uH7`))Ne-#CMZ7GGtIV}z;F zX~M!ZJE=V4Pgy=r_RFl^I=PDDqbc6ZT0(t<=>Cuzo<>D|FU$h1Nh0gZw?xOA30<+; z0fhR*j;$n!VKtg}PZJEG0GHPG+ONO-$}vB^$TpYNd(f{~X{%fd|9M-Us;klk87Vkh z($+u~XMInU&Ot~_>&sDVCRkVFB-US6vn713H*Acic!O4*L>!Duv-28?yiHwG8EMjr z6n?hJi_BW~am_32hLR%%NSa~~&YP|fR5O7_pJ@P39krl^RkdEd^2u*@Ue^`$D<^~@ z^((ZCO;pVHv@=K~NviI^Fd-=EE9zDID{Jj(5P3A#1E3VJaI)+_0^dPv*c?mE@cgV8 z`L`_wJ3p)CavPzWsoiwKcW4GhE4J zZY$B+&=x2;W;PsaeS=^2Ed1i*vp!-v>z`90fY}o8WayA@O_A$AU zmIc{u)oj5j=XyJ3pw}-~sa#IemaAZ52oC_PdOd^*iqa=j&M}6C%R1*zn2y^ek)58_ z9&HsQgU7)|Sj~_WCw$SoShE%Cj@-eTuS|P4Sq69Wk32uc8I9(pg6<0uQHyP%xACQK zgCn7&JqXgr$QJv?@U6~-70s64Fmd-=G#EP@Q=watMjd75vH$#u!AO*1`ySNohsUW7 zsWC^Ma|!y1V||}_zLi{}z3mv}h~F)}ZO$=v9mAKrJ=}LKMQ{EX@!y$xPB4&JGLb%v zFTqQ6l4 zk{~CvMevPI!Y^`#I-il?jlPV-ZcMr)(sxam_=O<_+@{!9--DprpF6|o@Pp#{q}R%7 zaHhg(wDy5KT_|5A-Id^+Yqyn1lWmDVBoL;jwAw#iU)i-dRRC`&D8yz*KryPrfUpIX(b!)&O~(MkfMa6idJc!6X>BbDLNPG zS!yv7i>vE;Dh1NoZSQqA*iXPI;IHYrlDY(#bPj%M0h=H%%b4WxHlh7htt+uN|E_b@ zZ>dl^SGLN<^N1emNJF*5m?2=UJm)$&M?hhLmeMLPy4X`{CgHGEHF1_OiALV1ur3yy zVy-=_*ztpUv>hxpsjlVe-u;8o@gSka2C<#^-gdl!pXVLGUa?fHB;=!$+-XSSW%e+? z{;91jOsmlf(<~}GGA)-{bm8YfEmoqsY|!i4g5sr*p)npxKZ0RM4;qWmXYBp*}g-W z+#3^H8r;^g4Ir7zHSOfejIQ2xOj(P`IDFQ*ut0BOn#1LcK260df^PsU6l!V+wS-|& zMgyNPl*xfvx9Ht>VK*`yd} z9G)PcJABtz@BUkvBG*vs6(Y1aU|~lTS_#=suU5esZwPBSRUbUY=wud=H0ca7RpvbP zt(D10Qqv?mUBSsz5ce_MyCt5iL)rr#M*V(aM)Q-DrF_U-Dv>-L%?X6*ZYt#g$*jE; zu=Q9i*#J(Sj zC!C$ispuGbX{$)b{O7hGfxGZrYI_&iNL(?#_8;%32v7h2&Fg76Vf5(}@5{|{uXF4q z@P|k|Ao8x(fxwpJhpF9J+T1mfH9!#{*i%^GG~_xijZx?*@9|7zKhx9sBphbG1QtH% zceCLCKn9bbIHkU1T2MLS4)8b5T zXjKm0CSu-tkBj_##$p=VrzSTAj%f|9RuAn17{#S59j&!;VemZsmywCVpmZ*?YKW3d zlxlTMkHFTE(znAIczC_f6j0Sn)8%>*w%4yMJ-G1jh|Qv4RacC)-7sC$_Gw>CI)O+{ zm&<*%T)5UiF3Qkn0~bbTr{MMxB{KiLN~%Ltya|uP^pqiOmRbvCI>|cK5AI~X9DN{R zq?`jbfmpM7LfsrI)wGuW`zl`9zD#@$)tgj^Z3K=_^F_N%ZeWNnxuz~$xQzPwBe%=R zhRj~@9P!AMB<%v=I!s`1A!UoX8DsC7eVO*62uGIDl{yYTL=1z>veA}_6b5ODJt`Zl zPxp08)pdEC;?q#SL6mD2xH}fe78PTg+;dKj5E#)j;yyIw!cO@-wrxWrZ&PPo>tkKv zV}T-_a3Nhcs;xqypL8L7Q)L#hXVF*}~4dMYdzDrfajY?5YOM#e?76ALu-xm|s1oH+VHjTrJAEr2x@ zo!w%|Z8H2Q@J{;WI?(Q^I#@y&NA;vxLSX1dUNX#ahloiC6k~GGqyhi5@Lp(bgQ%(KvgO&9)M73nDyff6s zO?V>|PT;1)x<=oVC=G9d*p~G= zqY9oiv9VJ9;E61;F&|e`W5+yBo3FzLJO+zh)ki1-I?rMTs?aAiPaDDoM^rB+@c0!% z>HN{`_cP-gzE%J)k4VY>GaNBYFzw_1~aV3aFmLV|$PQ zh}|w_c>t|LOt!Zv4`Q~>RR@`)WLWR&>(X?z5q&|y;%qTE{zeH9Kcmr)QP6J?{`vYs zjh!3f=}ha|`7U*atJKLTJE_mZ8VmeeR=3a1=mMk3)bWYJIjr&oMo?$(215%;poqCy zd)ar_s41NwXLR3)F6;6IJv`I!3^rT2Mu|nUFyn;pC3sXgYK=Ux>f&s zijAoi3JaNx+KlVSh~OLgT>{s79r6vdbv2s}eBA2(uDu}((0MO7vy~k1H(1b~10*hy zmHp0QUTWqxAISW6D^7MWZaRco?NSGv7~3OwB;05zC_2U1yU{#2uUnB$`SlL__-mdc zwhBbqX)=QBa)Xg50@u)4He{wBc}8U6vV7XHK=xdhKL9eGZ3;tAp{JODkuABR0%xql zJCuv~)2$=)^?THCil7|nin`+6fj<98j&GAx;2r$fSe;hbKKX2Z4#kmimkh%;zG?Z{ zmfM)@$q~nTe9>w10;^!X7@0!WDL7X@qOehSF%U9C4prg zkC)sSAtxOip`Hb}-|zAg`)_yt&M`56$PnBE*P+9$=SD8_3#)rTo6-Sx<;+Sje@-F9$S2k!Abh0^sS2TFj0v z@(R)GA7?!dYsj2J1lxC1ZQDc!CnI>UU!0`S>=>M4d&-j-88WpA#@+Pvi!cGp+~|yz z^bXHN9N``dy7?XOt*~TYAh99r~yOu6~&7{+sy$PR0jiA5>JZ>>UBvYinKF=BqlrEe>LR*|!Uw_AP6mDJ^qOh{}>JKESXkm|CV@p}}c{ z51-J8%}8bv5IpYABfG2BYsI|kCSE_8RG}}ULK`tM8j;TaBM|3$Ic69nug5?ggt0u8 zit04R)eOsdNV7ZekVXU}ojeqo2T|1@Ti1;w6b(+UO0j4Y8&I59Jm2>Bi1beL94RAQ z2`>&bKR%t{Hdr#XZWzGu3VVb<#C;)U;^R8dYH+Kgiu+jwp21a7rNXK?0@1Ef#@Zj^W2b43X@k?MeQ z&8K2S+;T+R?4Dz8Z3tWYg|&vtl312-SSXIoyveSoBPF9Ewts}5TK9=!An6I?>WpAG z9!x%#Wl{*soMe1VGGa!13{ntK|C2Aa`w@Q>MMxVmU^BBX71*yz95W#6J@&^qS2r7T9g~5j$|jfQUkA7r*J6_- z!tw4M#}NMgFCMM(3c2nUNKp+x)`h&3=s9R0>bUul7ZhQ@oH|lPBA`7EFMuhOa*Z;rrVMMMea_z6?FIpO=|+G zbtDeP*`*K9=kpc@x14Eb;Kk#Td=hiQ zf0^BlZ8A{oRxJ69g;|pnm+|w7E#!P*odyW);!+ ztKI!;=gB-d`QZ}6FvGXJia~*CBPtuUSZw#6FUw~c>7YfAskfstjCmqjW2ck#gShS5 zpmpto->)M*Hg+oFU(%(m8Vy;T!%g14R|Fc2m#qedmCTK8iE~{dj&Bhy3=WgI`i^eE zq|`paQ*GF?Ez31gY#dL;9F8^t1kP(k?N851MvF3I6G?R*T3$tiRboBP_yuvz&e8zO zA+=t?!hHKl7@tH^kj|Gh&AY72JD8_Z&C^Nn4%0>+Ex$)!XPYSoFjO^~X+0lqueB~O z@t8U+k7a4D+WrMi^jV9*N`s-2x|E!>7vo+*jg@3ZK(Ds;u%7kRy%KkDkNWd~>z&8- z7amh`?q?H8}#folZz@QaO@;Cg=3=jl`vkYAKifdJ_d>SDObiVTg)h zj4%7tIpunyjcmZy0b~c>n6kF<;KpS`6!BNLV;*L-W0=UAthm&cg~iICQ-hjalpGOe z3)eL2xuJbnyP;sn&$n^J)u&w>nX_$nPYL!Nz=i9}$&`NzaAzpbBRa)7!}k!9MQ-&e zB##NB^d{8x)sc#n%t(A1K6Ab}2W2{@LI!yE+14JNkn~cWs8|habvcB@&v4pncT&J2 z$pZPg-DFA2JO~7q7MT`wJ^`Qhd%aI#%L?OdF{fXKV zeoUJ6JWk&N_VB-*DDC@%57S}d^H}xI`(4i|o7bKCD#(8ORnJLtSg%`H*S|KCUp?iz zsGB=kZ)09(B%pz}KF*$6TTO|G?k4vGF8~|aowa-?f7aN7)B?$$h2|chNi3KwwEKWS z>Q*v@I_j2cTq(+p*LY<+wGvxlM>E|MKp}t;$pbyvMJ^&`GQqgn`nt}5f~B3p=lbd3k#5oEkMG_T4QO;s1&6U7=fGt8XiXna8od#KY*|rFK=JA`=vfYc!5m=6k6z*E5s_z z6d3K%VGNnkS}M7okt_Lgm-D9bc6$r&_gwy-*VWXnfp2YnQ}>%XF29r*_#F$%bj{yl z2QdrPYzqGA7Im?{%XbOuXqpMu7xJkhhFpnY+nr17O+(2_!d}fKD*=hA^7`wZ%pksY zsIr3Ay8CV{lO&Z-(OP|x_-53R&ke(x`%IM8;im~RAt~v#%&~^|FWBWm1hkYeA3a22#h2;_!^H=YtqQEFY)P<^embnV z=;9rI6?iH?whEcOkn?;wdlk z+p9!UeG^0Go;40|kFsfF5Cusaw`o5^tnxHy*PD@Y3`E*acJ^1Sp-$ku@#A%8Gp0!Uw+PbO;74ow1xcFO>mYER{|Es{dtCqc2lO z!sU_2gOgAF!qZ(KUFJlv0imwlGP2ShEpI%{V5qudbUkBqRt^2IrC+g$}c)ob10Ww>r zJ2-vU?qtH6S1S4=!~P+m3CTVmy=g1`6OHQ457_%qy(@$cIFZLCY-Gt#t8zpY-x$T8 zn0gg-`uTWdj?Egm-=rT$hMadaVF#-qRrRB`k*)PV7e`z+$x~te+)t$Zz9{I1Z4xu^ zecT6+vQ~NHfA=$&R~9F{nSK!X&A;8PzYW9bP4DkHuvCXI$)e3iv{bhfnxW={m!i2#9f>WuO47~Uj@x0_7>>AQvaBq$P(An{E&WaKljU9F@DA5{w*N{z zviCRYnC^E>`a&WVN;%W4_QeXH16fxoPrqdG`CuK4C4LtLiLb-tvzIbxylaI| zZm7_?$o1HwY1c4^1W`f0^dI>%qP{?A9~r9gy)_o`47jGHU)%(y1eDDb+WnP3e!g6H zNulD$amL54TYP~< z$geE@yP{t}pDW~7IYb8yDoW7B^2=J7E@{J6S5>ayrGGe$SB-JN66+%j3oHFZpiVId z4G!k9)1V0@dNTu&*Uvae2DzElptJ+kdh(iv!{xf#a^d2sS6{Ik$ML>JKf`_GXoc(9fa{!adnsL{; zqP72GTP*9cZB`)jZKkNLGB2`BM7%akGNUr6XmM4w3;c#NghA0&&fFB2{weP@NV}DsTJ}%brzp`%N7qha{V~q=>B)6|-Eu6U zu4#$Os`g`*YgXS|nwy50m6XCf0|J{XffqA5!CY)~`L_m)Ug^B zCD3k3`3@JN&Ak#=4>qbkyS}Xg$R(CjdQD?iWYOy2vWvGhZJ=oRtDzWCgH7U8Em?BQ z_=rE8XM69*p;`}B7JhW6_FyMi3lw`3-SFC9Dp_Uf>C_F?qV>C_`o&-Lo7uZO_Qc3d z(a(Fj@?&H8UU8&ir{2e1e3U8Md!)V$>pg;nMvkg`^uID`ev<+NEna2?3dyEWN#cn= z{%H1PNV+}>L0%7nJ~Gt#JuK#T^)`?D1crT_y-tOD(8t$BUSzB-k)8DiNojbs2zcHPpN5<4Ld0GaT;BzP&Z3tb}P&(WY z2^5;Hl-@bsg0jVqTcA+@QJPN2ungaPKiDP(${OF)=a^*B!PyiA+Q@O5(8*B#bIB*X zx$xlMdg-?R+UBtnpth&Hk=OFU`K1G_wxRKA+}QS%1v>NKpW71RVE_qaKEvO7I6rzh z$0ui;y-e205)2`dg?AMLhiMtAN&Jz2C@b$h{f3}@(wG2>`((54ol37-wW?iu>80&u zFMAon4{zJaHQ~U70}~EB2o5MX`2zPYA)tgQg%Gc9mc2Y9BqpGX4ReJZD3!hx7>uM? zf{1(F7MF0(COP@BIF~U6$pTkgFq8arL2Y&^}L8+JxmP2xM(Sp zlBufz{gaVpDX&DcQ2ciqNlh?3kudS_!tSsC2~2tfRmPFZQuc~yZTV9e9g?o98wzcO zXirg*_F~6veJfE?82pV=*_av^ks{{fw5|$$rr5(qTAJC~sR#*;npG^mcA{aIa8eLK ziIxY_ZvOB>RIFqWAa}W9R*;N1iV4rnSq;-5U6z9K3Q+#R6<^O;iVIas(OOcoG}gKU zRr0BGhx+Zt?K zdF-gCVXHkTiY;v)2p~pV21F!$7yGQ`f6hh36zEx^$vJod8$q3>xtCDxfQz(~YkI+x zG^NeSJpzR+vwoDt+cN2&wFehVb zrkzxZ7qZ=(ApN{_OCb-mdG&Ico^_W8y?pY>I(C{0uX+UVvJTc6Ga8-7wch$0S2y_x za9D%%Z!)s|46jRK3uehIY4O1F&?CYJ?fuR+aQQ*K3P8jTdhbVQ>a0J2g!h`b8WOuf@L z(m$#krOm0GxQvNxbso&{Sw4o|w5uKDW740))3gs|G0aQ;$Hc9d<)fhG7hV=$eMj1N zyfo5RG;v#R`y18Hd?}CRVYEBt7x#`qqjMYnmNQ$Oc~8kZwH4*1cFQAhXn&A4O-m)V zS!T-P`KP9p?aFC4cuEIiD3S?0rf}(hjDM?EGqb&->Sy7r7I+N4RyQ>s>Sz5kB#I3- z9SL= z)tN4nX7cP?KQiKNe&GwIyp!I1l#c8&;dA(9NrnrzrA$eQ6CiQmN9>R~H!Xs{*4_C_ z=}FFMT8?STCQp5R=^v9c`d6S$$G#eqvgDltT?hCGPX0B|$=RW-mTJhv7#~^$$6M(_AVM%!D$d&6i6)`cSILV{}gd`Cx~>-vx+kK zC)X6I{1bn0Cv)hGpuHPOKu4vk;H6xo;2yk7^WH=GW^%@J*fJSDgiGxmckYMxAq3oS0ao^{&jAi8-wYax zf{sgQuhLS4mNxC$W!cthGuxIVCfMSgJn)c?k|r!BZaZoEsD-OZA#OMlr(-;G0&`>l z2pk;V8xYZs3jv+Ar_=^EF?>GIX%^!RFFN#H1{< zXC@Z7n0_&SxTtJYhj5kKSLaX0+4fo=OcZ@;c> zhnL&#zO~(Y%Rjdr@Hksi?MwX0Py5t%`jCC9KvwUQ9P2eyqHH@1TgI;iQlX3d(mbm`KzY}qmd+M3?fhQ0I7J9FQ5 z+imUZU;lc${`%|NjW^yH`_`>noBoq);c;N}dM_zMnI!^DOoWV4xWH0GkrE0^zLTfSx$-dQ#U^ep71O}+yWMv46cpGneG`(i{h3HhMJnNVzjin}{TzV*1*IvBn z@fWqvU4K=({4;;hZsP34d_+Y)vT}56dSs&KALblI=89eJKLwb{hgs;`F2w!FLdJ0X zUGUVOE6r#+kyg689l|zr6ylL4X3p|U+#>2(@Zo723-s88epF;eREGhnpC4dZLcX3G zmu=75iM~Wl#rO$60xsfT=PT0Q_!p2wdZf|8#hisF@Ra$}MY8!(K>D5C4QK2YT3@eP zq{osuO8-;~ivWyc-T2Nw((30JcHW5Jh`!TcLMN^Bva!o5NZMI|#^1GK(zl|iNWT*75>)iVEM=!{pciy>>$9L)KIDR1&_a#8(ttEA zc{VUeBkvzf+tWEB%&R_Pn`Xa#ZNCHI=fPXrs)I1)C*a}qOaZbM>!Dr(w&Ce**TTC#XoW_$tDPqx6hG|tLD*D$r+gXA5DGl`-WSHHCb17IuRs!?IbqN{k95jv zQ(TS<@*2gHjx4--n%&;V5hJm}9V?cZILuNhOUX!h=x05k>>y`K<)BgiBA(^$@q9Ne zaw7BHLrCN%OnPMiE%Pw{Xvf0S0A^N{@t;1EIuH{-figx$<{q~^3o`lzW8=u%0>Y|( z>CdEH9C1rIZ84tVvSYyX(g$5AOcByun`vDet7w{E*JBDOnul@ZUHColk>U<{_I_&naSi+{yB?Mg7yp)1KI)K1@=tf_ zMreVG^2gRMemY0V!;FK(7V}TdCm8}GW*h(zSQ3N z&UXfcy07qY^n2E`p4C46=}+%7A7@#=etkRg$RpdyC!gGoJ@(iDh=(3}Xgm1egPYsv zUBEn)OQ7-ATW@VQ-+Xhs?z-!mec`j8{cP^9eB~^(1j#ng9s2L3jYu(5JsLbnUWIl52z?P0*S(hB8p;7esmyQVKssZcGg>= zaPgqDf`~=hC9vm6lvs;is2?roRSJgh^a;>M+s>qA|Gx3=cJkrN@mok&?+uoDS6i{nz9!eK8)5JxJPB)u zeg|u0y$d`S5pIoi2*|oDD2?YL)%Lgb04Q?SBK?w%4zB^p@iPsz5d{-RA7cSLaI13w z!=MJxm^Se|Uon6U;bhc~-*9}i5e42J%hy4CEavQEyw8r-;z$1J!|jSMUJ>m=`*qGi z4ESPKw12{L_~SNJ0fzz;4TI;5JGVUBE)J7uE^Z@E+Jb(MJzLG=0X+*^30(-;4x}vW zF>#xOfDR3>-9>tRkYxZ90T4Ye(R(RnnPz+O62TzSS+WFB(Ke;LqwUX0!?CEp>C-RB zEQ>wnxqwA>vieUsboR?K+E*Qxxl?ZJ5ZG7s3oxj^!&ZMD0zTxz_=i{VGrZ<9j1W5K z2$Xr%f^rxa_I1m{UFGcVD!cxIUXF!PedM+DF?Y3tHk{cG1Ux)&{h0v~lgXp-5J>Yr z;LtPLk&k;xa=7i5tJ`e=j958KVxUI8!Q2YpHqaR==r@a6bcyKWW~1I!>y8bLF` z#e84{8qZ|3%g8si6$_x1iBuM*#}8?7^`?$9D$n!>_Uk9wc;Y)uG zxcI5|jjvrD-ub@iA7SysHhzf3`nk<{F|W2>fBSfyooJteAB1^SL4Kfwd%jbB?Xbh;Mz%-uB8zsS+KQA_mIMfck&Dp+ zg`LTN1c;HYebiiSyYLGdFUwf}WmW~Tb5+@~5W%^{x;r|oZ|oG`B)C;MEx^&VtUE1UN24g)&W%8MV>J1 za$cg_o-K>OkNr%*(c9xL)MC^>^f9rgO-YmZ$^|x)e3pkjFPw1~XK;{?{W1k3kf^!B zM;x>Dpg!<4^N-Pp;_uvov(~wI!<>cPw5?0zNGB~F_)IdN+&Rr#&2>Fjw0bllT(A9mY+Cf9@m^Ui@6zxc)NkN^0O+durnKg@gH zb2Ib< zk87JZZw`3)NY_#gMl~G0&7)rejRJ~(P5a{?|9Ecg_uO+&Cz+Ugi37g-yC6~TcRafA zGN(*)U;Wxk^g!HD*imS3+!)U=36PFzk=tWJmqnE^rHdE`@c?EeuIV}cB3_X$R&$CU zB92L!hC({=0(c=UDVQiAOG_{=gfr5MHdB&NJTe#>%c_Zo!lXGF&m+A%SeNkl6GFPR z-PHt)ZoQ1IpjvqwKV>VDq+7x}bO|sG>XkwaX~e0AVe-&m%hK~8homos>6@eVuJS60 z8lJyF5Lc3nfM}jds>E<^TLvdDBy`jB3y&m0IQH$$FNg)ZPv0MAw&u?8|fTH{jN2!afam%y&j&bC+58-cBrDFx)R@&;^^10ex0LcEV zTRyvx3vcy>S2+Vvt)bYV#qN4H8i|B zLl(u$vTL^<)~f&v>IBWq(*4>IC%w3B*nB}izKSvb&((lo-W!g)Ali$! z04jd*A3hi$thVe59C`x`z>95sYf119h1dYr{hUQ<#Igb=I)}o{uA!J_$b4CR3#cSt z2nJnXo%?ckm;D z+KHJXNiZK^qA{jX-Q-BX^gFl78o_kZ4;c>y+~IcolUc^P`I*<~&k^tYujr)v z+)#wZ*okX;{#l8n7b7-(NX|hPYD&%+6(rQldNGbDQ;jz|7U{TkIU0(1nx|)e*6t3e zE8H`Zt)y|yTd-Ao#x-u^Ir{sqkZ9Zy!*diD!Bb<=G}Hs$L2F5z`vf!tic#j$RMgM( zu54Y~6{y9lFe$#>MV21(wUP)`7&#bwk;l|3+6pM@m^Sg&(R>8D1O=-_7G<$mPO8>Q~Nkm9&Gi0^hbZRz5L}b zf3VT^dHRoj^rK?|iY;Hhy#4q8{@>dvr<}5&+C&iaw}1P$7gU+=uds(7et7eIO|>si zIN^l0VMCoabbs{o>3{d#cL!Yj+rRzW_TdkIxc&X#|9!jbuDj;52$Q@94k$p?3!Zns zFZ&T^O2E-G!i@f%V2CrT$*L#-y+TB62qfC$Fbt=wC9X;zyJ>JM=(!X4oXI)$c`!~AfU09biM@T!RM4`68e#4$|z_))f@AjwZ5sSA?H*yM~u`6`V{ z-wKVgPyW;aL8|^yVpxIm&JpQbBUO+VNYw_7^h2F*c!`t8EKcnRkT#=rcg76G<#Pw@%csqSCM zSo{uIv#h=UlA}_S@qZN4t2KKY>}nsm@~iDVzjq~PrZE@oyoa{Ei-jiUj=O0>e*D;V zH+D?iekNdQinA4QFIt2h5XrCk?&7>f&&=bK0sw-xAzNY5PK$P}U=v_cou3Fhwc8w^ z6tXhsCUzhZ>AgTj+Lh)!{m3)h3(tIEJ9yQ>sh;7TXw zE2?#feFuP{ZcWuP{BVo*oU;fB5%3W|H52_vk?I^Sj7`UJ?Tobx6v@SC7OpI^)ZpSnW=5hGiV|%#qW%>&TkjI@JC6F_MRu+JW|>G(UA&_(ccBhB zJ+-v$xJRI8DN`_CS6{@DJ#7Y!$Hn-~U|$lq&n5V;2Jqjwc_U+a37TbsKhvZk6p3BU zJ^lj71k4lAGsJdvO#7$HmbJf^kM{D>HuQjD%+O_W`3}mtrJDQ+5yphBT+CSjege^+ zhp1h6P0i~aJ9+a`)h_0uU7GkG^Y|0n+KuZ1S~JPj`^xtb z>Du=kw|R%zG4n%*^|j9mfZ;`Xx+r5kLq@Rm8`-st=7hO5ue#2C)@ZPfUPaqo$(2WO z!V)0+@F&j<)BM~Y|G(7x`ha3;+zE~eC~m_P9NWgY(>)5qFJ~`83s;781~4wNDt^|{ zI&k?k*WStjyzvv5^S#gkL|8~a830An@s+Q;`)i)~-0y7rtv@t8tiDm{_x@6O;fJN{ zJi|x(k#&{>+Ug=nuHmUc#PF)T5>mXXdg`rQI+uUa>mJ>N%NJ$DfRcojyR{pxy{>%> zWu?yBS)R&wKYk;xs|*Zl;!`?`+tW$e47Us^Z{-VV>Nyma&r~g7&$JX>Dh&l# zMK=?PX5o;Knnd}P(-oYaS_BTr-Gf2a%IwT;IbDMb0(`tD^hO?~_&zK3<8G22aCX{IFo zY1&Yl->7Uunkh>Ojs+l^xO^JQWo9Nv)dMVQAuYExNBc@c@Rt6shN9Xo zu$RS;eOv&T0Q)ik`wl>ij1zzi@Pir;AW6Fb5YpB(W3)_YTfs$^U1?A79>5CV2Ue1v zZ&zB1(2}1ZkwvCLL)gGXDu6<|f=J zcPHaICUmt-&kRvKu@A^6yC3~eO+@*ohMqvR+OCG#mjn*u?>vFs_BafU%A-&J{__lKKY^}ofc?JW9ItY62D!q1wT*X; z<5|rqyqS`D*OFHHrU)4Ap=O`7(GANG)Q|CuUH|-0Ab3bRTh2MYJ@@~(V4PWazh(Qx z`~R~2^?&&YRkyA_<)J!y%z?h^sozasxwbv$dtbx{c~<5FU`%HD0jqw{vh(4lZ)JkS z%y`xfD@iVC#(ux0nBRa6r=HUO%HqKanu)Yc{NF=f3qaVl3S2 zZQt|Ksy}s%C}>@&oBhkaCZFhOsUSgI*p;2Il^Z96I8qn138Bp@Mk0*t($Dp;(0%v0 z_<#crc>BIv{YR$e=bn3R0Fg%~wS6ZQCqlQHUVi!IY*V>=uU7oaU;c92fB*g4>8GE* zSIhBWGI#5<=Lr7UpZ(c`iMH2iw`|$c{@uU(ckQijeQSHsi(b@@Ip&zQa^=dsUakjH z02g%vpHFzg6WT==U6lT^4Xuzbe({SV5bA-XGog8C9AL_b&{?r@_^;=1?QX}jzwJZf zqtgPGB?Ew$TSbM)h69Zw3TM!D=3j}4IOqhyn{~ zQ^7%jL;R-znidHrs-*~PqIC#RsCzNnoaed)9pg8%xUE{bs(tShzpuUcJ6=4|O8hq6 z>4*c5XlEaLHXl2gX;*V@n{DY`eSU}>zv}lRz*w^07Y6!)i~z^9+nF5$4uJvW5C96e z!$Q(_rj0w$vy{&_`vvEmok&~Guus+xuxvPNOFQO-bCM1v_%^i}ui#s6yrSK9AL@Zm%nviVnJ?@++WRd?J%hir zYQL^O`rLNLcm7gh4GOB2PAS{3Wkf{yRk16@1nSkPA?9=+GnTWk~4ws;5rhU4t-@LwUK4)Q` zM|WekM$R2(q>^}<1N9VC^ciqwMxb4<dH$;TI6G+x0IN z6X3rNuE8kB-g~Y><8S-c+uJSw_)qCGsV9&0h?rH>$YZ!u6~}C3tW-DPvaB!;!ceCp7D%lG(h&fU6co$zksY-a$oqu7ank&y+x=t z;v3%ZhW4g6y(yZB0=NtLQgiX>qmPc3?K$V1!@_O4U31MfdmS8K$UJ@EWgi5{M5xz$ zo`Vn|7&H62C!_)+bVimy*b;1tojA(<{feMU1XD0#0al|V^uZC4BNEBnMZQ5UGE6rm z7|K&g>9aeS7En%lsS%{Lb)lw088lwBL;GM%0#X2qct^=}3&Zq=KLSKPgjv%E9~%jwDi zPDt;s$e6~8ThTgU!)n7Y=o2S&Wv?_0L-ZbKq}#G#MLYVCrIr5}K5Z*`Fw3{0pA2Qa z@f&xxkFq#JEtbWGeBTb{dk^(5Ei-{e0mht@h@ab;wP&jsD~>4uMgb`AEo7V_(N_SHRVLb$TOOfgXEs`%QmOyF1_3 zEgy8}EI>f#XcP9Z0aQ$%q2Ksnl>mwI2%gOa5$!_(7S(b!j62{oed)kMwzN~8_RhBH zl$W=K3MB5yW549R>ExHBAARHcD~7Qs)i73l4-L0tG-H@?;~9X_e)8Sp`zsp6%$op% z%&?#p0=Z1E_H)oCVD|U29J~zJ2A- z|DtWbeQTGhQ!yRSGB5s(%P%^511IYY$-4?TIYlr9t}hWd`!Xx3d>&L zq1&HF03&@hU?TCJ@VZyE4Nv?IYzy=H#2^1@`<-{chu)MPH8P{E4i5wj?JUtwgg=G# z^S&j_+bd1sWC`E$8@~R;>2jDf>2UZN)U`z8KY6qXp!f)Mf(fAb@R)e~@yBNi=`FY1 z^6(ORn6cgZeB+Hb<{Uo{|6K@|3-jZSJ1*OA$Msx4fyB4H?QP91+9#cKQd_oc*+O9L z11_V2jbEL9+S8sEKw7sSaZz(|ay_yfh!DxE92UFI=0X6=n_UrL5C|}f&Tub649sO* z-JN=HM`RFyLUvBIu}?UoEnB&Q-vfQ8!$bO}Y>nLDJcwI+rbPUmth(H# zeO#`^C?Fh)#^t~K!5?M5wp`BJ7SS$bZxK|q9wR>-^p=Fz9B-c~x29T(IRlY2WJ|U% z<6jzw0*8!O`rWJ+B7V@RsVHrMiBs*Ur<~nR`=M76vM{f&Uw2*m;D7iDv|4XV4^nOv zI8&{l#DW z#aIZH@>kTG)EU+i_Ce^Glt{x zf!%8)OvW|mf$&u%1(`{wTY(~%D|N2;j}{4){*ZL#F*p?P`ehnP3jrfSFKH=jn3iGM za|RkN7aRfZ6pr~D0daFB7t3XQGa!xRg}c+5W@#&Fn{%1}P%H%~?K+bmmiUEyAhLAO z`4?H-^2W=!Rdzj0M}N(^SNxU#@k|hp;#m0^L14mgVIE+ucb@yz)piM$k<5cL~Da!$*rec62?E*jy3qj;M z<4&za0iXc{5>%$`1rwLjw%+U3Z*E5(cdm(%LglZ$N~CRkuRCtJqTR`D9R*LF5^9S# z?vcGT71wUMplv$q=i1E5@%aGUBhA?rYc{n*jy$)0{o0SQ1^TuEEaVGQr`w$QFH?8G zW&uO{jQwZ;4F|K_$jkv22aQC+1f^HAUHr7~d|z9)>DiBT6~-<-w?Q9z)Vb{&H(b%~ zx$~Bci;NR^>ATGFnvc5qqnU42J<&v5%&#+bwlF5sv2Fpx(pnTWEW04$5((Oqook6Yi4IcM|O@#cXP=-N+R)2_YpOOVPn{$xhMJ{WamN9H{M#+}>lY1eaR z`LX}%j5fU#p7_eQ)B5j(hnvaKf802_+k$8xvuoKw2ezfFR<)Zx{>jKf6gy>CMk;yI zK10z{Cwo%O>j4q<=Vr*+Q--_T!qN;&07UzTW_JrJx}CY)Gw^K5T!0bUH5=BqC%)sY zZ3*RCsMmYG?-lK4K6+zM3Wx|310tq}W%z~B4n=Q2z|!r~X}3QaHy8Ho6vAzojZ!gU zEt<*mUK|+1A$PAbPCWJj4)}Fn@A=uasK${e>8=Uk15K!YsZ2d-nm#HP%u-NWlBou80>0>WYF<@SN zphvjNR-aLtSa!3M7*~Dlq;fZJ-J>$#ZCRrA80sotFKo*1!)q`UP!*7@QN^tQpO#h8 zHI*77Nt4_m2xXB647Bd?9Q+bB@$eSU8Um9SG-^nefLwOu*;)(?q~?`y-R5g}S@o&Z zOjl2?()1{Pj64%CxTvSqAILFFD2$tj#ZDcJGnSX-PbpKn*l|a|)qAjSy~{6Rs&QEL z5hU;|y?lfQphyjmxUDf@L9`7869flYmEb7Ru#T%`zY&r)tz2=s=oT0bV2pinP(?u^;%7nbTt&B_?FE+wkh3Kiw{h%#v-2d5%s+O% z@xB}brAb%k`3ZixjXNfF)b!qP;w#%>r@w=-u)FLx-nDyZrCYXYV>|WP?``|9-I6m7 zmm!XU&D1@EIkT%RTRPR2Y0sCwca}Re0L`Ik314P-_t(z+v2n}!?TicmueR#I;~rW8 z7htUAt2eeM|LYI7&8NO3{b||I$Cd|_2ee^KEn}?O*QVVT4$lN>1?(3yM>`xHs`)4q z^EB4dG-FWufh^xrOR)P6^>+LPa`eYB-;w$)(90h}1yikqcEQ*B!LRK8@aK<+evn2^ z+WPwbw57;Qt{@W;6Nc^+cGLei+QcXQ&<$zR-%4YVG>CW7%fF|sICLEp z3-kJupa0K%LHh=3I%ibS_Vxp1%UH1IeTKWfWN)8Q9=FcbQGAw6UGi7r8q8(bT=&RN z%j6ny!0|e{CLGx79Ps?P*T4St^ZId)d+xcXop;`O?fbs(`*y4Ub*N9j``zzm;>qfG zx3F<__;u>bF1u`A`FCHuc>eRB-#+-k54M-T{N-)c0{+G8zUua1{nW_xOwRxI-~L;+ zdoQFfh(Fl6&L@3@fRX_QSpliQ74V#^1ckXC+9N$E8FX8T64`1qCrm3e3Lr_=#Nw_b zqG6Zp1gaED2rt5+7`pi%=8xot@E1Q3w;?@CWHiHFB$T_%X!4mx@__~+Fn5S5qC&TM zOQO9%JK~!}(i?!ZfhGZ@<5rN^lNISAczOv~B|GT#8vSx>fy|cGC0pr+byYSC6b*$=hg+a>r~zf_HV8}u!|=q@ zZlOn+=s#vx3TzpzNBx!E53Xc0PkaOZ4B8#6%Q%Khps2v2`iiq)Q2|Gq0*Ri7C?J(D z3hS3}044kaRxLyS0YB}F`QXapfQyUTA^WdwZ$JOX+B1%S#+Whdu~VFb^CjpvwB5vYj5REyj)yx#c$%ke%hr2!&0!YKk`p}avZ3l08<(SvxkuBb`l^feB z&w6iLbI5T40Rs|7E44txnE@OWAQW5-uo#eW02<+AfJDK?fz~q=H=XwV?YOhvfwuEw zADF#&i%hi>pLA(E;R%-v8omrmcxwL(kccL@VB!p8bSZPGfTCcceKJQ5I8W1G9edM` zVSvQA8Gq9Z*cy2??4|>{m7JhP_dr{O}vw%2lgJfS|x%KtVyk+y(v> zfH(ky#49jS6A+PVg%(qN)@AgW4nP*D$b2`@ETvEBw>0@w9>zKD-~8)##B-iCW(<1Z z#rX{9CVt|#J``CyqNES*Q9l^^M#4;%{?g^n^tbXVzhpZbv&ik2B$;kD^s_m6lusf~ z9&n&m5))QTI4}kr@aw+6hf8Y93Zez>}k?W3ZZ+g}n z+u;WuzFUbVjz^vY1u9;~w(>Iy&;^i)wjltZ+s*q2adxSNsKygug>485kv7eiK--yn zaX>}r1T0XC3aBWMhu#(C~ag!Hx8lj1+rWzxM2l-G97H0n!2ytxvZLFc!GD z{*<@31CIK)<5lQ+D$Vqg{o3Q6{@%7~&H8|Yoc0s#L&4U7gfYu5z!;FR`#~#R`M0)w z`Rca$thcwrAGdD-v-j0ho6r0KK*jF|tOn?jN6eY|&=KP@U%&=roCUV^uMAWCK_CCRP4ih`o7At5d5r*XGHSW4^U&ha+V}7b?e;g znj;;2-y!Juh1Q?b%n)RqxeK%z7iC!ixT)P9uVq}MLo@%zo| zu5Taxgp91svY^2j6St=8MOZ*P8WSna-AxL4Lr7=mM-gSU__w{ZKBmifHY zO8m{={LS|4XFq#Bvt4j`J>dyY2&njjKlp5B?toHHsJjO&YV^i0 z+Mb7GFt228pdb?s&B%^Bc_?&~pK-IeFkj;hg+naIR&o7uWOr*XKyH@SIF*KJ zRaqCifDw%#4Z~U1#I3EiDpeTEfP|*Zp&in9&n51pJJhG@Rs4}hoa&dlbbpA$L*t~r z)K9;(19tN!0QV3NA1`G+d7|QHT~&v)iQ&?j&Ok3s?;|VGyc@U69+>`Po%mUNbmQCW zWUaK{arZq`Mw?nL)D*j5kbog9jv4S!H;dhXivbP^Q(Hz5D&QjA(LXc;9rmuFc=qPA z+MAwR10uQ4`;I{f_6{My921xBx1aL5Dz{_oEp;Ua$&2nG?&q}kE znAjdW9Pk4puYK)n=eb)G}0C56?z4Gg%l+JXwVrMItAsZ(Iu`z zvp1`X0fY;n3}_dvMTEaa>__ciCnh2O0S`olxceDHA^-(cnvVGnWr9A!w{Zj+6AxNJ z!67Y8(=p!&@uBJAMDPWzRGWXIzK9Xh?86%=1JP4K4Ct&HKdzr>-Kx}=SXsU zz?UWTD!8!v@a{0;Y?#2~jo-KfYQEwu^n-Sypdz9(ZVt8&s7Om-x?QzvECx7)jivF1 zDL}FMniPkQ;}yhIyjFI*R&eQ|s8@q&unp5`o$8Z)80`gi zeA_H*3#oOYUsLV=QJMpeePvsH#7n+y{C$C7X2r&K%#(gLT814wbUSlt*QuFU_S%|T zXC_875ly>g%h$By&V6rNvHzBRfmIKpj#KTF^WNUp9e#R%M1~?}eF&+*MBL1$g4?=F zOOZax_*%mFn}OMWT|3%|3^2!J&O7CO=ns6}jQ9YFrH$3!77wC`V?t^@>O(Zt!FRkh z8@;b*?P@0GT^+-B`J#5iKYgx!?oa-VI9@*ME;q>y@?EB{(2Vf|8RX<2zLYP`uMUVP zkSGW!Feu0vH|7j|pnyi~q3J-P?tTy8qv7y$1U#A+ehl48&v5Q7FaO@Qd;y=E`0)?? zpG^(LTytc4s##ui0A;^`gw!|vgFxL~rZQUGz0VNS_}$+)y7LO$lZLb>*S$HQ)IGT- z92hGOTyVh!?I}-r%Dk4z#pw_I&=0kB>vlh!*7E>A@rh5&Ygry{X6Kx9PJ6)%Uhr^} z`IhldJn_Vw4d};U=4y6+%SZs~Ko!4}z!3**qYr%G1I;hQPp${U0blXH2s|fuSvQmLA{)50!kqF{di9PWLKS4nK1E3|JWKcj?2jD#K zh{eE8=)AMa6&T|#FtKavAxNu2CZL8!pn#j;-68=x?c4zfMGgix$N*O8agWM^UH4Rw zqsrSf1K{;Z1ak?2A&YpKxOcX#(NMHY!3{t|!9@W?fs}q8Ud~Az!tJj4 z0Eg-c`r(eBprUrn>0x`V;MGN^UC>^5#=n7ha_vnHyy8h051NXMbGJ#Vl_+4=!9_Kk zu={(3{!{~B2&Ca@e zlPmX~x8qz)G&gmgW<0C{NL+Q)!rj_;-!*-BzRT8ZY3on;{%p_nJimVKUv1GHKrhb& zWS&rqv9|1T^uZZF)|OAe#QR}BpyE^C-uBynV{Ly%1HPZB=!Zup0P9GFm2g;f2@+|?apZZ2or@#Eg|?@ODIx)pE;s7OZ;6kLkjGYbyR0*Quu z9%9Z?gom@*h{i3@u&1TjwHMV=T+0Vxj^;xw3-$V6zxBKA%K!UA=?~T+U4%!@mUd0x zyDL}OHn?660r$WMRz z)1zTG`FiogW3OBL-}9dLJb(6n{$=ipkMbNOodplL0Hfl zkpXw)TO*RzP6S>BkOK0g>gN^;P#IQq0$|bzM5611r(l_jcj!QBiVl0|x?i~k-WDzP zH)P5*(2EG^uQpfEN!29uDgZ;8BwC31>hqxS z7NAiu@ze|2g(sgs7QC4hqBS)YPdMy^(o7V1P#aNjarkgfZGC1ILH%h8KtgcPkF-SF zG$5q@0>{u0F1Tpl(r!8q4R59}!1(HWz0Xw-6vXa*6v8pRvNd#=R#;cJB`emq)yM2E z(~W7-=e<}59Q~5EdhHp2U+{^!AYfMq4^fedA7KKH+7CVX4Q=)Mr_Osh7gC1HS8ZsI zf9j6{8hRcgfGlHc@ghFX!Vv=jZRwjcj!nYUqFsVr4MqE^kcXOyPUE`W{)~a!GzKk2 z-*NaVe=OLG3B{U4)3R(Oip5@HStyu{1RV2Yui&F{6>+`)_7~sR?qN-qxyi?H&9Qc` zu{YPAb9OtDFF%g}KuXN83s@L1k+vQIhT7poX)tmu0sP69X)4}8VS#-(f>*bHiw@(kc1_6~qr zPV?QBaOy;wJ)VGMz|*0u;lWV;uC|INpBMNV< z_ROlb-{XF5?-ppRbDwI5KK|`(>C)9`(g`RQa5&OPWRB2ZExWaR75l(V`{u`N9&8gH zc<7n!=u=(-PzWds50?V$`;}+UP;?8v8s)hiqx=xW5Dt)7XDISs_* z-UQ17F8aRXZ=qhkgL5t-bqJ`~>od$L)=t4J(+<|`HlVVR!}2A%skZgT8{5a<^Q!}0 zhm|CnV_!fl+Vrx2+g1Yh(OrhK4oe#m$!Gu*3uNqHX^zGr{X(Fy?9jnKKGI$QBmQbH z=G;WmKuU}KXg&n9Y=Lfd-p09!fAZQ(+dciJw~EU9&sxUkFL?KY@}kd{nbB+BScuvP0sP1A(@O!oWQT6z7lcJZ{3-%g z+z4X&Y1Y|-n1em}4dFZe$jOK<2oBP(U>5NO%mn^I+uZAS{B3qg`=dspv-9i}D4(;)}pyX-_6xJ9p(_y2gdpThI`<0B5uq z2VNDxXgkq1DT{3dE&Q&w9Na_u5CExdZ{JvicH%PPj>}~q^eW-E7DXl6mMq(!nhL=? z+ZV6Dm3pX+Slge0h_XoSXUI*VA=AI=?B3O|-ezGjTP$TU7qF>=jkp1&p1k=<6EHDt zF@6s})bgfhUDj5uY4S7&Wq-kBBN%KVwmC#nM;=iU4eM4A5~xn=RA2 z14z^U`60w$`(FK_zL7rX@bPjUjh1ZhvgJKCPp5CkfJXqVIQ$3NqGcP#rflO_km=e|ChM&% z=OyN5+}dBY*AJ9vHZo?_PShR1jcH%dUpzmcBJZ!Ms9!zM@{AXqbBN_n-7=#Lb_Z|j zg&Da?w_90baQugJ5^;UU%Q}u3yd?f#r=d|`S#pI z>c5lsl5ZvkhFv6Ys}XPr-)4IN#Qb~Nd%QYbKR0|7LU8(~^g{vy`Cfxjj>+?WIiN&7 zxh5PKOAZ`z$RX`@uY29Rm+koDkH1&Gx88bd`%nMrKecVq3YqsJJ){f;N8Q5hhi)Fu zCCK&TKmOwZ6cutN*Q3LM=RNOv?dN~~=XcwN|LEkjkW*ElwEPMRB{=g%i`Qs5k zEP{*DmMCgq#giamvymg<7OnA#6Iwtu(&*_R!4M{B#j1d@Fxpa#5Erl!P)iVxfT9!0 z$vq3s1w;&pCjcjHfqX@W8^BJ$t;_=7SaMWe0Tny=)`2+0M!~=lYGfJ$bAumeBkrU; z1>Oziv5d)sG6W#3{IpL&pD@Brx8Er@c@(IrKl$R87ojnK^EHlin3ic4G#ujMAAr=j z)=L_0JS)Ay9dNRzXC4EXSimB2@G}kpG%Vq^LDDF@ZJ98`4TGj=r`i*bT}hnrx_VbE zD5s0Rml>_ z2inZa4f#PLT6FM_?v#7!%8l)ylivP7l9|xV;lT9L{o2V-c`G~w>|u`0P{f{(1u@3l z^4x*N0uOP+_iWi0Or#%A`z|BRfQXpURK&+O*+Q4Yb56DM2r`y+bXLhty}Rrcc?$oS zgWRU}*$#h9mU&#C{owDkJ8rnKCZ_oKs1(O?f*O_a-j+3d@%gatex4H1u;p13EkoRO zj-o)LU}6Unag$88IK#L2Gbsm5G!5u@-M1Mg#8sTtc*FueH}R_9{=N1EKE`7=NdM?2 zt_~17@hcJ8M|?NMUpm}%z9M$`%&;4lspH zt_cUmmjiyF^rV#0SoS=RD^*?H7OX7u(vkYae)|eSuOzAa`ExBf%g*AN~dS5jM~j z*u?8iAPV6#pp`(GD-7Z17Y0ge0 zzF=Vicf>VJAdov6fw)a`v9$3Q1VlW=Zo1L@BfQ562#DJ-)2uDZ?*8{r*W3afTbv_8`%w+Q z_RRJ97#Zg81J|G44%v8WM2>77XPixgEM_P&-U_rGG!^N%0*fk*`;%pGf0FV8Ejd4t z0WOpntv=s}c|SbxVvQFimCVc7y3N)_St)aj?`!2V=N$^ZbI0)Gh4tnZD2j#}$=q}M zZS61L@?&lL);o6B+O!S47ua<;Tc6i#J|>_dCk+HJM1l)gC_sp0Mp$Va#-A`v&rx(X z7d(s`d)IW-ebO)dRGVHkem|l>u_9fkT1iypCvR-S!J!Ua@0K z-`RnmcKriJCO&S)TBkQGfMG2Fq=vrIvI{BySI11qE!oDp%qPxmkT7@Cy!t(nKSV6}n)Y09~{YJDS77faL=51Pb{mNcY#w zN>2Yl<1l{Adzyl50q4r?mSur7X#*@KyyyZ34*rG(B*a}n9pa^S(zJYwr#Y1;pk5G( z%gG{?0v&Nx_dvhLk{Jl z^VOeTbN&!^KlkNZsF!}6g_m;_>8&}35pi3fkuA@qrAT7>0~TZpG@6VwEp0?J*@gXJ zz{EE_;|=ZLl@CvoeeZK~-zmWbr(D#wY&f3r!tn+D11+jS(VjUI6L3+0Q0>INB|1o( zXq%v+CjDoza~5toVfs>lXzbMqUAWm9t3%q`imv+N?)`Q=cyG~{9DHG$S~}V4{9syS zX8$wWvIEZ?H0cWLTDAUyw*1fw9!xS5T05Y_B_=@5lhsmW$bk?pW=@V~qFUqlxdqxG zTU+!CSX4uJn!wubJZK{h$<=me{E$%u78mN(-+7wRP|VB7`cN|p+pmyyL_ zEB4Gs@F;VU@wxBdJjJVi^S2f%J9aDgVzxdX{pQza%X4jER?~3y&UR<&jD%DlXBq5E z;F0u9{|QSR!|)TNT)IHdO}qmz@h@KYrnZBQ;LonnI;4(v3-1mt>My{!gL9}T~>t8|T*Fq245`qUMZxRdMN99X!s(!J|4@p+7LKuy6H zyx;}%Ub{2TJhQ#x6|dMWU%|wm`?;UXFJ!v~jIl#d^hbW=NA3lVe!C*S?QL(HfQjFZ zNsm(aIp>@cP;pN{`%%hn{F7Az7+&K2Ro=}K5~xDL6-rAO=vs>)ECl)MKB9riuz>d0 zN<{<(QY6;gwT-BR+M*x8C~5rlTmb~yU7VdLn3Zk2q#I2_R|uhzPQ-?Vx`1B?`trLn zaTS;c&@JF2wAo^uI0ecTI45w2*a%Q3nAkzIMN0thIuJ)X<|9o&NPxa6!}T8kzGw(0 z4fCn+xkH>42_#X0Hi0xUFMZK1AwERVosmCJGu+K@e8%Qd7SO|Mhy zT9<^U9)sDpdl#Ro{uQVAS-`GfBcP#thX;bR(NvU=_=}iHXM17E?ILX5elzVEC+%nB zkHw`xoW;@5i4|_O=1L#!g0B80K7Z-PTiYG?+zko9pb# z`gy*hhv3!p#*n}bf)SGisAnebVp0r1;do(wb3xBwNgs3}X7AHXue3i7HX#s%NElc(wy~N7`7Fgk8MZ3<`VjXz+r)c zN;ux#U4S8dhbqn5XX&%u85Zw<>f_snmoMCpDSh?>ztL{L{)V)72l&)F6hIsSh1#jB zeL~PNO&_~&t^Q|!8vyCu!tksh%!=FB4VwJN-?*k9Gmiz@xl)5i@3HV_rbmxAla$AV z0}kvT{J|e=d*jQ(0*aogIQRPf-~avg;SYa!uGiSxPdVk3cJaj*Kj3n{_O-8VFL}x2 z45A0jut$OL+0TA<`-z|UiRQNRef{e1eEFbY5i?IcxI@ri@+E{TgqBcHv3CYVL}-9a z|Gr4pTa6umr4&dEaJbtCR{-M(H>@-j1+#c{2DB2T!9D<40SN^Y1=F}kT8e;Y(aKYs zFCZZCLIc<)UG<|8Obc+wlfEmOcs^GKLwt+I4FLB_VIG#LdJ)ntq|Wi&9Yhvo6E zxK&=`=q69|jwU2-{c_7=?tz!#gZWqn!sTIrQB2#S1H92rq+J9?%AZAv0Hf{2{gNk* zmlob^zfNzYjP~I!1b3U8q6>QTEFiTL3q(XaQ9u#RmtAc;eN#{o2o+#4V50ydw_kd$ za}!A$O+_)I!oPR5iyr@7?X-=jKtH(_8V3$qd2qY%@y}-{05|{~3L^Tei63eabI4-b zxlJua?p*zldsYARJK}b;dSgASkJi)P*)M&DsknRXb#w7S7qqFFiFV>c zX^5qVT*$P&I&;Xfb>Gz%PqsQg6gKV=XR2*D_Bm|^&@dlcQB%?5Cm4TfkjG>oxfR+E zvxr&b3(o@nyssRRZh0=ypK;8fj(voJ?2+99aF$F)UHnsaViL)Y5bCDJV%~r}1Pp6i z>L8QBjGfAD+=5EJ$duXCUeoV0Klh*8o!4`oVp70xf?A;0+JAjfTXX8k*#aNXkjfxI z4H}38$Vi=$glao9e9$hqC=fXfe^#tp*N%M4>lY{oTDuA`@tQyV|DwFJ1Nq0j7y^gc z>Wn!86!o_sL~a`F)ZadWk}Gs2>qom0KTZ2!+`Rj;p1>Uwdj4aF40B)~;gjpzbAZu2 zxh5PKe-3Qeu%W%?HLscXf*x?d0r{}W+^_FC@%Vw3n{U2(-pliFG87y=@x&9qZ4ws) zJo-iF$+h=6;CA!NF1u{+7i2G&p#M6UVkl(~moNN!*X!4Bt=NY^4Q~Z;|BPOKrLhPQHh^*kiV2P( zA_gQATr&1Syc5?lnr=@&z~n4l0MLMlvt=(pRnV{KYJ2yPP5{9nzV^g5414ma z@&|ym+?@t9>0)Nh#0eX9T1W2TvY*Q8=Pu#q^J^iHBL(drxqsyy;KlI8H zW@DD1>+kUTj{fMlpZez;=%WCMfI%)w1%(6?7fmB#M`KaY(8Ve@-6~)rU0mR)&QGL= z12P`6AAr>3zH2-fF)77E;=t3kJfj_V=n3?1+Ejor(-L!;nur3402t0=&S4cUO)bz% z4N4l(4YG&E{^(c4>%Na7IdO8;63t%evTl5?37GhB+G2{WkuTxQ#EoqkU&x+Zj|>O) zKjhSQ;QG@7Ch<##z4n%sWhb&Q4_Ll??O9lxwf0 zk|~7wh(D(fYD-b_0!)!I#rV!vE`wM8h$fIEXk^p^5d8oL^AB^DAfxv^x82@8^RAz- zv`jAfB*}&MpgxDQ^?B*4RRIx^q(&Nx+L45Yt&6K59aR_9y) z`3voHzy6zfyX>&-sh4*^L+VyQ;Q$&MmhQ7?2&d_xQSJ+9XgHm!x}E+5m}vhYEVrG` z{x-O!NfJqS=t}9BA(=exhXcjI`w5(QKXy5A(@i(E>#ye%>Orm@zb96 zwD#TK{oT9e`-Lxjq5bkN|MG5r9X?Y!M18t}p|pV{~Ph$2zyS8w784kB!o@Zc|$(hUNG>RP5; zh)#r!2o|KBuRIeb&=}3P2)MNXce|iyO|~!x02}~8>^Z|Q0AYpae7^y3izcG5Rk$?dXWZOH-_u)^ zc&1(8CrQDXahtJJF2=H8bs!^TzE%N3%<*`jwobrsq}{ zSKO_hj_INbjg7C}dPlqZE4Sb-?Zj+z21FD*45*0N77?a9_WCeO+=7oz{g^A3t!S4% z?X^R06JsHB;H7MRj+UZ;F<*QTNX%CqnDYWC;&hvHra0gIgMY1YcN?_* zQLr$JdCc@h{e}+e0xehXunU=ild_LE;FqzN9`?$Sdt!MA92kbiY|N>)>6GsYaOgJY z#b}MItvJnCb2w(}a|amx0YJMbBV!D5+&oCLf4~Ha@|(?IVcD2O{Nt5o<+2{B=>$Kk zTGbuieflvcQUBWNTBa-(RNBr5TjH;q7&m?TGwpM~^uF+GVKL?2rJdo+%!j`0#Ut== z1S-O_+73Nzcg7vfM0gq+%(Z+i`hf3z#=R3=06uqKcSHN+Ti+27Fkmrd9s!E>1KsqI z?R1I&&9uAkqi8M?W`C%+m*Mr!Dmv3C5HjyCJCXg6XWncE3=Qg4eXa%4uO##RHMZ7|w>)@e)BlN7u5l&1?IyHU} z-Mj%6JIIx-x`KAx`LKu@bR9qw-~(9mi_7fEi@gJ8nso+Z#SM5jXcI019Na~`+U{I{ zpJ1KapaV1#S1>Uk9&xfII=~#^F$c{<^Cw(Q!DtJTMl==y63xGFrS4#$pkrw>n$M7z z@l!6sIID34F6zgfdI&^XrqJPMIY}$!H=iLanuyTIpZMv|J&KFJpkdcY+|52{Bg*&i zd2r((j<;>rLEy!0-TuqkJO9mLXyg$#HkWYC?mcx>w0QfdJK`~Y9eAf*1(@i$iP_%l zc2$8z{F~dH)l>v5%om;A=8Qe~E2b=>yTTgmeE#vGlb^#!SSIHtj(EO+EpB~&?kUfs z%QH^}NM;rjm>7V?UHUtvsj(=S=sf1Usb7Fb+`TJ`U~r|+^(TRS!6l(~=SgWM*JFzV zQ;S!(C2J=iVtJ%nRV~G(OVKt9a0rOzP-XlrW-Jz{$k-%P?|i@o0?&f>)-ONsGu%e9 zOMD;cQqE6$ic@{+a4z!P;M6<;1Y+tVK&Z^sw?2>juyk;77fdX$37`o29ZYQh^1GL} zTR#22=BF}`axMq3^?CKFr=WP=!9v6~?eHKb`TM(};h}+F%h5_)`?7_5T-9g!qVrZj zML|H@x$dFO(=W8sg^m7$T z9s@$iTZed<%6T2R$L`|Pvt7Z7_iE3$Fp#%z6_o5%L; z+w*~zJMX-6uJ>5meSyE>4R2`HXYM74_)|aiQ_ZdAlWY7qu!^k@o~L-!QAdqm;pU-4 z*}~9Y?wbB;O<$gS*|!x8O1sw8@@#m-0-eUg(5g!vLi*RI19BpU&0YVMU^ z2h~bTZ~*TTCP>EzPv(GfIcqRr-2mEY=QiC+uiPDE)L$@f5kQ}};H2~&kc{@CX;?0R z!=y8$Hv$DIcj64;$V#d8Q2;fxNqf*# zOq$xw%k;Se9O^d<=n_C14nQJpV;@;}0AS(^#|=!hzoo}jKcqvZKiaRkOZ=@Ky65ww zpSlKOK*U|!>AM1m*at8%ODg=;T14cIe*i{T9HGSkR6Kaqf$izXotrXD9*+$UJZ;M} z+wx^A89x9MK`Ls?vx_I)Q=H*uSTr``SK7PIq0V)lqnP;=w|4trKt$LxJ73Q6gT|}i zj#wv;2?r(|s9|v*d|(UeG0#Fa;q`{7Q5e5pdj=c|Xy}hV1K>~W5>;w+1yoe7fwtT4 zBZCFy6sWEpk>O_5v0K8RjV`Ix1YI2 zu-^qhBpvOBd8My~#L>t_yunuP$@5+usF8dx?}^7_odZtdcinYYK%|8Ph}kZ$e)X%{ z+O_k)eeI%)E^6nWfBtS=LqPWZ?|=Vpe&g;CG`#fEOLr^g8{hcGy|SHrw?a%D-;x8i z+kgCz|FNxDvEp0OJthNvne+1ZDlxlW`h!P|IX<|hM?mNHQZon1Vh|Pd}8nV5-tml$#4K7Lb@N%>r9!ALc_MqBa4&0Kfpl1l4pGxYj|%0^S7hN<*)} zxY7U=kYnp|07yYT!#YSuI?>*%bfh;8&srR9TMlSg=?WqiP*`OcX#plpXgV-RI)Zxv zA_2mG_VqVKsl4f|rk4!bV2YAG^eu&bS@-=$BT|FejDohRm_ z!-3^XRG#P|skGJn>bCINJr|LA3!&XfA_M`s%PUhj{4lo|Tj zYd6QN_rx~gz`n=jq^0jFrEG3UAM)id{Q zzwWyB>9_s|LKgnDX!WYL?#(|G5RutD=WYrnYWD*^yy{1O2cwxdy+F@R{Q8yut9_nx z6K4TLC7b~mq`muBGXMYVodKmie?C?eH@tDtKG6shZ~yDF&bTG#T|)nzT<0t$;DN>hq}EFvl* zMd^Zc0!bjGH_|hi`hR}Ud+#?hDFiUg%$Iy;=6<;gJQb;&WNG1~{n3xBz5Y4MEK|>1(_&upk@jW#cQ`^n^@c}d%+yZdy zEdiFipf<%NFfldfa+!0cP4KPM#`Cgffo-W}Cr}KxKs5k5PDjQMz*mBd3E~xiqvFue zYb(G!gNFv>lwZJ|mk=9Rl!hPS3Uk@SX?d~%JwU%AoOp-zMI$iXZQP&xbjaf}&~B`R zQGTZJAufI{%HSTlWE&La84UbUN9DKXp~4EV&>*MdGmJeJ%diqqG!0b_^g@f%e&tE} zm0+$wNHj=k2ibtZV_D*S%=U5G5nIKeemzsffiqKZl^_fIig^v@6u*eK`By@Vt#Rk0 zvz4&fSDj9FM%+eQF~LM8s0w2_bVVakz#||cprdR+#W(MAaBZoxD7QJ9Lw9~l3>+|6 zeMLOaKbncUP=~_C4`^tOMfDT-u9wC#0LWYlEvn>DTH!uVyb5^RG$X$p9mm?*Bx`fz zrdpj|HU&1&A%=~8P4w+2En*2T0Ed8v?2AaZYe?K9OIz|aSEh<372aZXT?A-*p9PuektYtO}MG!0tf{K$8{ zUx0=Q5*t`lWw4tHptwc#>jnU?5zs^_{Tq1%Ok(6=LAe-;#%4r3$ZZJF+gc^s+)N%3@AP|6jKyv2PX?~ zQ340$BYYyvTY?~?1yz*>0;4pZbhpW^a7_hdrA8bFvRE???K!K1CD@1%ock+U+;bUu zNQm+~j(n8GJ+KjsnP4!pPeG`xi}!YD7p=vJAl}*3n zxE)D~qIX=snihp9yl8`r%XWYs z+{A51T`9wK!`*%%FqU+sttM_YAl~&lE$7{RSb57HY9|e#g^dX=S3zGK{!=)A=qoN< zwmSa&=qyb>^P9a7U=dJ=xQ*r^e!xVu6cMdaOylqsp8}07P-)E25pnqBgMG`*>1TEG z>X^A=X3SnOD;BR_6pI@c$0~(kV+KllufG880lf#r$o`{ZyCLIZ+ri^vyTRLSPGktQ z6c63yt#RW+nnZ(AXc7khXvA_CV&(+r@efylO0>a4bWy^vi#`fxCHG;Mmt63q(v{1@K6DfQxlP z%Xpu<@42(C_a@a7NL@Xo@czP0U2{)GoCbCn!$2D)j99))Qc{O5!!=r$mE@>hb|j<(qYFR6Kn3cE1KudH=Uf!vV*|It;WU}Esd^L z^XPZJ7b|s0tPk2z+uK FW3?VHu&)<%@uz@aJ%kl0BpQEA9$35k59k&SGPjL%Yd z_{Q%wxy@_XgqrsGh9ay(k}WS_{`~oI=9y>4_rCW% z56;y<0rr&Rpo&vYIYs@PVWR8I(MKO0H{Enorvq7Tx#bpXdVcVO)w3Dy&O7fM=bn4+ zI)}}49VKgbdN~y6-@m_4>jq3;v}n=GDWXgAAm{-UwX`4tymUjqrVMxQ@_kM*4nqjQ zjaO-0e5Oap1O#l+7i$~Rc%tmYPMeyF5=IqX0*6ed6`(8$R2k56J^^6@4D+;SY4m7{ z?xoS500_CUc3c4_mYQ+^i~x@SVR9QJQ(0&}av%II!}mid=DxB40uYB)C;YzT=Fin7 zF9cC~Vmlkk;cIMWhc1|Mc&wx^90*R7iy>*IOTnEm->-1s#_$wnn=scQq%>uELb#l# z!?Qaczb>B9Afg00PnML!&tf`I05qU&RFcEwSKXD`w&Ua<*uQtYdH0d=`pH9LkMY&9 z6#!q#6TlV`ipGI9>|F=Z63F3KP@;w4*7)^Z(^P%Qi;vFA*S7?q1SZB)ykI z1276G;$RLhcUC^w^2puZw%KSW0wUfs@9ubLftE=vc{=G>lCz*Gw8VIXESVC1l3|P( z)qiA68ZkNc+iL%qG<=f7tNrJY$#0H7J$X~CX;@i|CE_FKqH44E82zVBJmVAWMGGcc z9-rvU;y*}6SjmI(6(k{qek^A~K)Q=dSQyhp^y~{AX3*Eiwi}Xp!Fz#oHvpnQ5-5`@YPc{CyE_!gr6UHC*1jZ1a zZ@Kny#p99RgMGR0XpB9Tg{iSvm<9(*O4wGhLwJvp@u4T@Pbcl`tK-I_Kp=xNq_`F=w41n0A zx*B8@J(yM4uW;PnXH)sSm!`t_5Nqj3c~Uwe4CzUHB;LVRG7(nqLACjF)s@QB96g!>lP6D(PkiDNK9##CKInh}17goT z_l!xCCK-HRuwcPUe#``}^aAGvFUv1@O3_Qc1ho{2*M=}OZFk&hr`UhLedGSWJ{XTb z@s#3N)}S&F5K53Lb67}7O}LCBt)+Yf4yP3$C=k#9q)-`yiQcO(m)hpOeTvtz+}z)< z&{hOYL{K(>C|l(Ma@x;x$nU(xa{+AfY_6-~sFEl8q;z_O9P%j~+jWu0buVHZZOE;N zNML{*2|pqnQ7$2+fRt`|9fs|D(V`B*(YCzLCm=((p7kubwHWv}hBrCy2 zO~6D<`aj^I!?VXKRnh;Uqxn+~2IrEna@9gMh+z;>Zccj!M1@jRlX%?uj?8fU ze4y5*Gq|LQuW%1)lFUTJw&o@eMbNWF<6X1t6|?6Cpw;A0epGp?+w^_y?lJg%?}}AA z$r@T<1{`sC41C++>8Y~c1-IWBQ`I)jstiD)FSTcr+*}!6C%3c}9WM}3{RL1FFwx7M zl^1R>@TiVKm?a}IP6;H!Z~Rwy3f_|6G}KS&b~=?sxZ^s|Q^}Q%Ol!wZ_vuhN1B`ENL4}2hQyz$1k=bn3BiQ->&nVcBTUXRBge|+q)!wxZW zk!^WB)$lyniC=OBJ`xR$JM& z8kqdK44V9TB{&8smgy-spPew>%lFIYBaY|PPsR5062kK<++dviPRFy`jm=kAqbg2o zqAAjKRvKtkrX@?#0+);;iN|nz;+$AEc}#x=CIDEbbZUMwuj74(_Ap#3kUvZoE4RH_ zA8Tc}$hz1{`yW!zF+=;t-~qk0A7Z~4EG@(xwVZjYVSTkHqG(vfhAEQU4+5JQuDENI zLH>R&`je8s*0}!8>B=J@(c&>cVzKO5Hd>2lC3>Gkme8S@D4`k+MV365*O9xuwTh`0 zfkxtOvu};xK69fdo}O{4dfq3spd}WJo8tz3y`VM0upE5IOuS%l^R&X-v~+HyXKBiQheTohvOU!Za}fU@&Tr58ZkkjLk5|Wl4%x_+@YNkek8U@xv0s4qQuReof*x21;@vJAJrHS!c z-A_^0meSaSpFZaRDP z@~@{v^MemZ^VI3lYwNKw^k4s}Dl615cV0Yo*0~j+5RF7Y!UPX=!v?$dCZ& z;sP)qHf)%LH$;uVH7 zeC#nps#`m8@tVbP&ckO3^joY6r|Ji#AQThI;&K>nBGkW2xm zmVzB(NT_ttP1sS?Wab8Et&$^Wi~*9x)?lsLOehF2?L3Y zJcyz7(zHt;k$$IsDU=Z{;x-4W-_f_-prw{#AMk zvZ_%rLv-r%vd?@uhW+3_qpnJqazFf~{}(F+DzZ1CwGP#0v{kd(tw{hfU}B?~pd*;r zD8RW{yaAw08}peJFwyNynA({xLp<*=-0&3!UMV#e%Xbw#=C~r>9^JnTE3#JncGP}0 zNP!xlxIxlvnGqLVbdkZZU3S@}Y6!r-g)G_b>XViMPgweV^2sMx_K9QAVlT>l_uV%} zjT%)oG_1AXJv0k=y{6vHGS=CUt0)`5nFG)bpR?NM53wq zqCrJo?CMVlnR4W0*0m&D4u|W6#6G`I{RzB{{d3w2Axj2kPnuk6G+RK`2uTVEh?dL^Nz-+6O zg&doC>|wfu!EXOM=hqJ@ET=_Gk6K%s#FuC$Hfx!j`UC(X@hpXwpUJ5&Eq5*zV%a;f zTFuh}_bzH*R~2K!y+uHp7P|F$NVy zFWb(;WHq9pX?0vP?N_n7Wp%u5e06>peA}zu6+d3^glMM9vMdmi$O*29qrE$-G#pa)qR{ltshP;px!wA{YpP{wi8U<<;SRVxjjhNtl09!OrbILROC$?zd zVcg~iu*_<88sFT<^izmB?R~BcSlfk*uyyOKgJ-&a%_j?IK?=WV^h82b15_X zVxwfuhhN~K-U64J__!t?WaA(k2Jw20WB&N20S_6EF{)wtvUvC_-;8~Ka&`}=(8eFH z_oRsi6YHg+SdE4y=u4iSYEZBQ1_2JOrD!lQ3??RU2tY`jBp~rfE^SWGki77S0YusC zqi7(JaQIANz|qQXH$4iz<8v+Vtn64yC{L+*??YLLB3{Pn(pmf62nA}hp^Xq|ON(B* zbg2O`PAsmLlYh}ZJMFa7;-eq^s0WL#Y%}&u1kmgV=uDe7Eq?T)AH`R`@)ZwKUHLql z^S#hIeBglxtQC6aop%OweyL{JvSq#n=v=Rf~>3?Dw+`~4kt)KT%qH@-2( zj;)@TG%rB_?dz_)&h>h!^w`wt0To%|oL```bliZ7EYZfiXiyO;K*le2l~KOg5kfOm zU#WZd(LRkkNi*?;PGF)l>Id!Akt0TUEb}vEVMq`9m!PQ&p~Fj$<+dcR$u{6(9@fGV zW`M@Rt{ljsS7%)wx>DY&X4fdNF$%zbg(zzw*XaoqOM7yq-*U2b?O&$H!X+!jhXN73 zXQDtu+4a&!%%!`zQJ8s{_%r>O?6s;cids9_CIt2e7Q;O!ZPM^X|vu!ZaaWN zgNw4gm!jISQEi+Wj7qPzbs8}PYqJ0i@pGy(Hr!xfQDt*Tip93fz42&DQwq6xhQ(haGlU{KG%|L$^HTr#|&5 z?-kea<^WZIUN!UgQ((@VIdS2I7aEXaDea|~UiyMyVlRPOeMXFmzGJubg}Z+6FiA1% zAMRKx%|4u;{p@Gsz3+Xm!C*8XtHrQS;c>?u*X@tn(9jU~-FKe>!x=MXbb1~HyAo79 z_Sj=z6llwfynLA(Na$FiBI*V7O%|AVvcbe7w4A;3DZh=4jq%vyPsC+cTpf>3nI?e( z@j(Kt%JElg<cW%x zCqH2Y&0R(rLm4(1TKcNpZf#l~&8ufev57hw zSX3ZGyS2+SMxlAea@ID{%u8D3XIU&7Yu--}TX|Uy+a$U*DXo{vDouh`?EuuXS6vrN z9zgTIX1baJ?3*}E+KFYmC6EX}m|!8`Um7bPmDgJ#zy_XAyFsIv^t&m zYoh~)*m%EH08`VF#nCu_Ry53<9;;_ejfQDYMDwyGm7-_On&o!8;f5Qm^~!#j)nHCK z>7@AeuYVmgXU^=_$7RX+uYUC_zkmS_x8HvI%JXyt6$#v!reg7KcmDPldJ5JEd^u zBYP$qT-0gLvP;cHKVa>rMuCk`fQb^k=Pm6F*C+!FSeO)Ub^7qqhWM{vJ|GZGA~Z{z zB|5X5xz3u2Xlyi#Ezxd4Fh;Bf`bpa-I};9Gq&!Eys`{2Y11A3Xv2$Wola@KFhR`i% z133|}2B*P7#cC#yV4>16165k$0S2Kb{kfbh$SJqN)Ta$Jx^(D71X=aNtVFc)Yy^+m z>)Pqp#!f>f#kfJ^6j429k8SpjJD&bsQ70&&4AFBi)wl)elbhV%bRm?i_TibT8 zS3YW&->cuCv9Wr^a~3aH_KcQRvRD!WZ8}KhzNrq&5XErhcSy!``7>{_8&a1J#^Y|` zH+!%}gAgFJ%^1NwEE6FDHGAnZ?*T+Jz(k$&tnrAw_39bh5&79?S8S-OX3}5odTT$M36%V0@=6m@Tyw z8TS=Q7-@0W%tk3tKVrm)uWvL}Ex`*?;NXJ~j=lEU>jl&Fv`8lYXnJkPu=JUepQ&55 zn9-w02ZwXqd+)t3>X8{l*xPL@PHFu+4F^Iz^UO2xXAIqQ{{URE1PG#~Q(5Y)aI_J<)L93va3lExNu9XuCHgAGhm|J zy=D6V76FWCC-&BIuig@Xd*haN;#NDwn|D5F!;jNLW3HQVZ9Ki~X-WU03;#@{<+e=k zFakBv#LWz$NRy8AXJRVIB*DKP|sFU$oEKGttuAXiNgoxwTbP6OWnIO1ouE zGzoxQwdl@R@!VChdcmLcovB|9SS;EdaCY(2=f%8-zY)u3-xRBtPKhP~v@M$Pw+fxs zx80ol(|w__`ZDxwZdep+mQRV5bAJ~r=Ug8x0wDVgnh?GE45=cH(xa*_9WTZ&8*E=~ zv2yX#x@L$L+HRmlvWPC+(WXmwU8_D?wra2DHhmaZSg(*RUaYT^9lc}tj$>k*H;yj~ zt}biA1M_0;gL4EL>3|J=Gz0c&)E#peNLJUd zL%4@(|L8rT4l7JjQ34p%79PVq%rlndaNn|gWvrSyJ+^s!btnu}R|~KIX`j-eIP>qh zFEtL8gr&)iY9GMCMztrJr1tAYZ@^IDsj*mSDLQ?Ei_Fr>3*gycq8i)XON~ref{ED` zVGzPF`GKqmcLqS+9E4s|7y}JMI!AUVmr|-}hioI@ip6M(J2$*cSKPbB}>yPA=}(uV0U* z{J3%Byq6+?UU!=kR76wv<${Wp7rOdZih{!PO3lQRP867kW@2YBk<+dL^)E7*_{V5! zR@(z2QU)Lapdg|~v8)-59Kx^x4YBJ58uDe_z@fm!Tmmhdya0@{0TTfo@gINW{@qRP z+WmVx1=>Yc_!>>Qlwk>P|GVnX)8h6=W{LX%6FJoxFtJf?Ktm=_TulZR?H0ILF95Ml zKqIUg0FGJ$jdtQuyT3KY53eq*y4kB{#nscV^4KdP(N#T!n^tn$pp1$9j~|ncC^s7=TGIQDXsK{ovOJU^&i!oVER#Un)i=>Hni|jc9 z6?d&Z0BjSr6B>z&rkoXX9{i%TJRcS?yC!J}J)y1ChEN_WFM!1oOf+EY{_Ae*rR8gu zJ{%1KYx@k?M!@34P4twna51Wby8w1^8BkQc?Z?)&YRNOPblwxu+ARHZsbsYoK zQN2tcB4DCUYtfb9qSpGV&vMDHYxs_%suob3|KPls_qVwMcnUC4?>YVq9^wLA)V3bj zyeC`r;iH8WMouFcUSYsR{I&>G%yC;|4{6mYbNI&3SkI_MljPXsIL0ltO`|mB`sjq@ zVY|IbDQf1GqyYOSK6UN|1`V6kcGf^tn4KG|^$N#thQOAwKb;l7`Kd!#&H3Ufpb@KPY7|&M1=xe^j5E%N zHEY(aKS2)+<$w(iXz31v*3~CJ`N>YVUb%8*oO$M%RYk*273f0GAOHAA?*;gq-~6U> z*E$`#G2+l8WAeAoi;185S`2&b{?V&%KYhJkC81NVAEeTCUJI4%FYM4u3*fM2O@k+* z113z09X|f0*zqf8#(*7nt>i^h@e5!0LXSA~r1DVfaj-|>kw+f6o|x_lTCiY2Tz&P` z@rz&lVx6VfytK>HpE=ZMeFx!meAy#B_R_%u)=WI{1;E59Qw%2l=C{9(#wJ7#xC0?m zf~D@60i~dklVT(cmNX-P2u(=4&|YNUMF2%#0uRe&&MHyTD!?KrPR-OPurUh2M@pj$ zmI90@5D90*d(|D!#INp{t|W#l#9v{wo~1*zBrEZJFoMLTJC`XzpU#G&h;xyO z!tHz%E?0+AT~?X<_PMwD69j~%uhMXX8vEyz3(I1BXCa7CcrE3bj_=0#dCi-HWp96-1KY~ z9rJ>w&`k7F7_`Gp9O3PBgq{VB-43|hv@Cd5%zNOov2^NrufS8k0!1>AWEWYh@Xmq# z`j1lIQ<%qex+W5Z#vuhUwjrt@uUKZm%@|e4FWiD0)oOThfAq)ZD#i}RBbCZ%q2?Sut$?Ln}oC zDt`5=UkyO^o^05+aBZ!f;DNhMZyd-{e<4s=vO}#CY7s9vY1j@4@pTas}rO+-%A+#f5@M@Gk4qA!icO0N3 z%c5lyZjh4rnyFFXrBMJ5!yvjM>R$EdXXENWKO?RyARhsVo*rpAm%e%^MP9fG*HlX> zbEy|%H4{=aH0rh13vhAIZFf_;>M~Q8KOIw-Po=BL0P>Yo%6i~mSJcmtoNO~0P+IqK zZ1QEFuH-8m<>Z_U?Rm+d)}Rmd9jZllnuF}rQ8t$3)GXhJotr44-; zzx08Ia5&C>!bz-zVa4p2doQ5kmT=HWw)C21PsFo#d@xoo z*MTddfq_D3XgVg%pwk+IvI*AW2Bcl1^lP-vG1Ed%feL_+O5bcPQMtvSwA=D$FYC#y zwUK9|Z?&7={f1b=K!1+$$77JYB`YNua~S_H>QD4bu5ut!sJp7;tMiBYGE!u{)14zs zhP6tK)qB|~je$nd#CzOn%rdZ~{D6sm8_F$qF?dMa8p-ftA%_7$CBfUYY z)_Ca4-;CxJD-}{Ro0SyRo_DDZ+5KudAP zTBo+YV1{mv?4tuY&{AYyxvn-Wi~YBM`?nZ6bZDnr|Ih#Y&kLSR+$r%E8GC_ z|M{Q)Spk81_3Izoo^WbRmZl)!9zY`6h#dtglSnU$JS+o%r^BU5;LDb{^g?NoMsA;> z!(-xSza69BaZIH(*w^t#Kl)L{-xD1>?zm&Of$1BfM@LYRLvK171j-Ur1PompROHKi zXE5;})&Wf6p{JgDIxf2SlDOeFH)(>RnFrf7d+FVH@hoC>`;m;pn#&%VPNTzV9idsZ>N)2U?CyVSWf*4r<41~X7quTfpf z$2Xu;)TI1qVJ$MzrCney{iheliaGNP{Gxs6Hd0$v+J-RN0h>0I8<3HY2nK-_ z?l`rR^U9{(y}Vf;Q2m*;;0<~Xnu-2XS-i{P8{TKc0X8bw27YM=?K7<8$s$S3dKna` z&4ykE(YCZKoVE)fwzh!Zlj*Sg?z_i_Km6ftc|`Vc{K!W>(&^sNkfqP;DOWQ|0e~R1 z{?0qEeK||NaXZAsFP#zFyywIKSVSYyGeHRSX|$KfUbcS$XXTOvU;VyFfP-j zjlWl|6!@q-GCq_=X~|=avdZfu@Bc!-ty%}g1Z@0?r^U!O|6?WTx#ynilf)|lJP{^?-?Lr>C`K#bDwMH(G-f**gzg zN%0}4#R=Yk|4ANE0^&%>C8wncUWa%953z|WY`Oi#Tm>YEtxeR+Fr}=S8U;2+0r(JZ z@`TBW+v1nEKNna3Wrnn_^*MfTD6x$Z1B7A)&n#S6hqH(N*$OYAITN6CD`Ga(pam2q9!L zJc&VCCSv{*4`xay+u%>qDCxDqMBNmzZ04_Hp#a5Zt(h#BIRgkbN>wHSE!>*e8t6i6 zPCyu%b6lF~mf%;qNn7M4k1m!?TT4-8qNUfc!o94|+aH=}y_DHu$W) z1@xEW3-W}zr3azxii}Tq`Ejt`0~zMwi67Eesc~KO?B2VUx~Og*J5*mr504=FF$iQ7 z2qNg?kced9P{5+WMWy8}2)N{>u`0*QWo56?-impWZ~{^Wr9?D9DL_QWDPIY2&C(u! zGq1c>rPa))qQJ~6uZd@MCC$};)uss~LVw%}Q){R8XuIu`pA}Bqk_S888Rj-u8^Rg} z5f#t7t8(Gj1z3&EP3@k%T9=P);r(%&N$?d zL#l=lC!c(>PwMRs?aC8RJTX}6eE{_=cD8ju|s%z4^L3aKHWbi(Pl!H6DNb@$P<Z{9+x3^czh8S&ojd04t;@ zftX;yT;4j69GP5F#H1Qf0U=i5vJr5d7U5N4?7N5{OB_N!cKS2EJQdo7)_!Uf*a!v0 zMdB-owBp5;4NdXG|C<{3KBbf8C05sKV|r^K3Pj{E7OwDV&K9NR7g$&TivWxo7VY;Q zifA#aTn%jstBYO6?i2(24A{ui^sMMWSH+D}e!zPs-K_>sGoBuvU@8+$ zd`V$vCQc^Pq#N2$R`y|8cpOm1ceY(l5ua&^uZu^QJ`y_(sUE;~+u_@1Jw-rcgU~=k zF2}vxy-j?nx-xD;Gf(mDmP;VD=}7epS22`2q^JLIBJ-lB{#U@1CZs|n%H_#`i!D0% z!hn^kk9MI?Ay#F1efSkpRIX$jc+i*nA4~A+3k_kXF9`v$^uAc4CA!0Q_~2%GNK2b( zx_X69>(vm2c47%CLQmYLr)}|%7{(L)PK=ZNkuJvA#bH4$q9=z1=`50L1066$p9>C} zQv8aA_r~&B*T;}?J!h}zve`S?JoXEi`rd<5zF=g70GNW|@Md=XDlFsWRyc8$^qH$F zf2m&ri;P3K;RPzr7$l2Y*~>nPc^nJl6??X-n<`#D1~U>X9X~#hQT$Vyec&6S|2A7uV9op;6ky-PX+OO*K}59!+J~uW zsIfur5;ROuQGVUa`zNYh+rv5lzY1^Fz-tXe;})cEXY zKilQc%RY9W``qW^_~Vc7wC~>czyE!ooZVBwsIDT^`RAV>=bUp+<#t2%dP7V&^&8P= z_-KQPfQW#Jjf>{Tibo%aCHLJLE2liFli8kYe?e4ocXp(HVuvujJo;`uJ_hc*M-1QZ zpcuOQ>wF&$WMR+57c!)&A2=ws|H$X!saelNFdo_uY5XUmVxpa6>dm zrKA+J5gHK^fENfuMgTlS&_JX>%uo{hQhspClslB(LZ+QxSK)fwN2pz*t^P z$dy5+E%q9>`-^;*o|Ejp#rL*9jQdqi2k37G5mi6>V|HdGB@J~JkijzVUfMg-U>Sgg z`g=JqkUp`}ddi?Ia+8(5Z&Zl>T)K%U+_up~c5(%lj!g5&l1JjG?cP;H_Izu{QM**? zP9;qZR|(n?f(N27(PFv|36o~&E<=bf;zVHok*6pj%W=VE=rLx|lykkeA)0e&ym_BK zRnq{NRA3B%$q(2F54E8BqB?mwrYf9#qgo1i;Ga7|e+fhc?3Pa{l*ifHbofmb0E#by(Dl+%tZU#SsOl$LSYxl~K3MQyFXR^S zsCiJypv&fCXj%TCK-y*9>=R8`uFL3z$E?kVNBQH-UQ<*IlvWQ zp?ORloAC*$j@O2hD$Z_-gVxKI#lv6uX6$>x*$SzdmrH@>&l9n>w4 zgQnQp%dR=)L}H{ToptEDlR_$h&j(jeq3BVgj{8B=5Cr9X-(U-?K(|Na-^ z`CGM|b{%I3n0fv?rQ*^XB=GA`U8=J-hSf^#}lfihzy(@-P2l z&BXPAiPNS{6PS2OT=(niWA*BW6f9L)gayDCjNSUhE)61Rsi{aRFa1+ke%+K%A(KM8 zUX2JD$Qf92m!PD>eMe1p02XVeMuCk|AbIQh`xeBPFBO=$1dVDfYu1%Wn7C2Pv~cHg zW+p(b0xcB8WC+a*`OVt}fCh+|d)FyW2@_Ooi=D?z+UV2tqy($AjCk70Y3^(8R`l4S z&(YWDh7Jc9BwVvWcTH@%DQJZIXU2K9ND#9|*;xkec0cu(FVo233A+O_qWzwv4ZS7LT;7AIv8f)VEf!8WUw{s@Wcew}lc9kwXyr>Xmu%?b zCC$yEpY}*>Y{G6&!_so{m9%u6{R$V&p=s`;n7}XC!Ky{~$LhuRZK}#_+Hzh3`}giU zpy-0?O@+)-=|vjrkWJL>WEk(%x9}8wwusoY&)$Wlm17HI234Yd9%HCx2H5Xd=%6&s z0ty+U&~EcSdUhvJW$iZ6$EOG@KR}e&$bJq2@e*4V;w8=sF99y65v|o-1whutQtie4 z^jVwtRt2j5D{7wBev~U_&#Tb7q&c)sK(W}{tt(fo_X!7&92G-G zjg6sWw~C>owXAv2;OHwY!e&YQEzOOwdfC!guG58=E|?cf=g-xbvSpphX_z@J8m^oc z3x9h>j6CEWG4`Fu2r%4Uhq2_c=}w6Y4{^iyeN&8j+k0Z+Z>}&e{r7+WcU&wDxt?^Y za{26&Cr^&ucH1o;eDJ|Cs7Kj=isf>5z{72}*{1S*2$!QrkM>mwEF5Siq9OWo(fvAY z!(cE`=}Tjm{00*gh@4>UB{|RlAi$ua;z&z8pbS`p84!&W)?5`{zG-Jl@)qS9#gMNx)ov*VHHmI^b}6i1UH1BK#7?~$kvDO!Uip5$VJsVS0{JR?SX z`7^*HK%%vzBrvly8lVxrwnk5i)MJK>jPb+AZQv<-X7C)Jh^xLu7b_moZUcx4aaZF< zk#t8V5|spIlqaGZ^w&=-S}V>%;xa7(=`Vppj}L?qEV~3BvAJBH4M@G(krW93Ar-^*${s2Dk8^6ZuN80y-SZLK28cLJx`Q&M8_!6I%t9@7 z_R?l`pAwvEVjsl<0A#PfQnL;XMF14c_3jDf1zHgEy002M$NkleG2?Be6GT)uRaUNpi&Uiv(Iuj)J4OqK6eGfVqW&c5jn3DBx3av#gI+YebufDqu& z`zxv)3@GAv7)#uXIJGBf01rzL*<&t#wLAN2(&X9Y#0&bPwTu7c8n5A4kK4i#{E`O%(x*=c?Al%HEGyMpCx7s$wf9#9VZ_GB@2XT2U`qOV> z-jx?bWD_$N*1bS;&K*82qW{m*J zr3>f9f@hzJXa077O#AD-vEZ3@V7F!Y(pYf)WwGR*TVw0@ogAYMeWwJI-s@imt?~c> zwtCm`vFzU4{fvNhmtJ~l{PREmbEoI+37!E126$Z05kmf^7D_9LnbOoS-_=ZcZrU5Xzj4cWiaGO(q- zsVaKXNnO1hFlQ3(tI!2aO?ZOb5CLN@uWlFn@?QXki)$CZY z;Qj;*RV_3I4IXONYHd2rT3H5-n^lr5&(&+g00p=P-O)UvALUNHFBh#eGHI5dbW6H&0846rO7jS?(ebIN>E+~riGTn} zRI`WogB4BLJJFyTbXPj}|30Fp+6B5c7h@eDEn&;%-5Mh&SJP7R3ZqoIMK5U$x3w*i zzlbZwD<}lx(Yx#>oUjbY{0ggHLub-IokZ18bIY55)X7y-9OF}-M(^62=g3F5v{tp^ zTllc1w2flK-T;n#h;yN063J%nw@OFpxOvYav9;S}c)8D_{7V1uSN>n@`;&8O`zg{O zFVZyVH0P(J-I>~=(n3_*suk=f%vzw8unvHs()geprAY}XbuD<%}%55Kc+Uf?;H!lU2@kEhnpt1-0;Z2Z zwXqX+imi9tIrcd8t+8s!;&}Fv2jiiee;ZHy<<@8tAcbj~H!Gey=bN$o!MkJJasLwi zx7{JN71t`eO@L^B9qzJ~PN077yze-ceIWPQXP?-6@4eSb-TmIf4m&LV_kY=po>y_t zH0-H}rXnwTK*g=M-g>ro0x7dd zn*}LML43ik$QqR6VA2)>ucQ@gGG(UPZ;b+-C;*W7>%T75^?WQ}Exic#15-DIgAjq? zOg>O>Vzads9VVu77>QC$21w+)JK!Pt3^o>Suk=+sf2p|>u}hVm?hMi9G-%rc^)c$~ z$)%jxhqEtcA5}uY2>LQa z>B!01Xek0Bdbu)mg{DwQVeX&MNi>6OfV60RvOg^B3GMr+GxG5SL%-E`7M)Qg`r0iG z#pj>Y;d;w^=7Bdb!8(S(tWO!$r_Z3|7Xx2Oi!zQrWo}Y7(@haBwk*c{R;I2#+x<&F z{y+ccfL3j7H^%#CIkDEr2Wd z4(&uV78&vZ5IGPCO*pg^VY|BJE8@x1e-L|~d3yB`w;P@qF!8}p{kv$)hXh@E1}QJw zZLm-@Wwr)j$li#JvPqLy;THV?5S`a?jK{oNECD5Ws5TUL6ydnDpZEhr5y$vqIYH@? zkHjN|lQ@snvTJ73Q=kSYZhD$-_9fCO&pPX@IN*Q-V(i$lRZ(B|Wn=mASHALK$xNCwDJD#q5L2g4?MaW-=~);{&{VwTmRo#~%nJdEd<^)* zAN~+4SFI}K1W6E`OyNgXKvI+;gh?TjKqiBhG%YbAj~3~%^c!aQt6 zJSaDL>Hr$8tw=uj7#mSqTq^$6{K;<819X)B@x!;Px;E(FdtmG|c*h)SRXzHiJ0lj8 z&BAYzb&Rtcnvo{^Ed9}bQ$gVvtd4!%^i)t1Hsc7RKhw>96>ezT(O;w)(RTzTSB)92 zCC?b|87rDr#y%=5`Y|czeOd0?2FcdGG9>8)kxW+oGA0@v@*R54&x(a;C#rvGhKg?7hp@sv$CGV8l zczWOHKBQq;-in3y^<41lB{f%DEYU$MSufR9^nh;GMl_8JU{T^$C0uk!Fc&^CtBGEs zssC~uWBf9`Ws}ByR-q~H?OYPg&f5kUI2)MsZG@$xg44}!gF867TQO=z&@F|ha&BW z{G>KlKH43y5JR7VKia`WeBuD&qD1e>Q*x5(1Cj*KIg!B#eW$n0+;@WKvD8K#o z+s6@aKO(NW`d0>WD-wbd7ovn*g-A)8rQBg5(q8+Syf878tw01!1QRcxR4Fb8z_G|& zPQ1zQNLd&JQv3wh%$7}om22AK?&)jd509^oM`o@zAjoD*I=CO{?Lwd&->ET8HYqDl=o9HsGW5(`ruhL;G4J&4~ zYbK(BDDaED{?JBb|35So6HH8y(V(KLFB0k9fL+u_mR=VomBBC7mw0Fd_=o}Y^738L z(P@QVSnG~*+5}o^pV0n&cIh5sY{I)PopX!9LTH&+J1r~AodFpE6-7VOx=1Th7+{5O zl3Q~8VtWT@$Nq^xLeLhr)E2U`Nu&3z+dx2t+-f@QPuUU;OP}m{fFkVR`ccOYW_@8q ztD>chK}OuAzXV30MQ*i%)@Jsy4Hehnjy>2(g{Tv9;Hq=P;Iz=C<2zi(mZW7afB(JNM$ni@nz% z6+;7Jn`1v3%l>wEJbT7hDqv#0PIf-{!~Yb=edo-WwD*3|Bp?qk5uu}Adv*5h-(S~& z=&hyFy(EG5(&@Usd?Oa$ z_FJCi_ulu!(4j-KSV;<$Lla4Eu;Ht7X)Kr`P83a;;34rT zypxXngjYm$6&q zDgWU917lzx_LZ%f8U;F2U_^guD8fH79xwGC_PA$WG@}*cA6yCFZp@f_J0*HCxjJQd z1wGHtxL)8DACCaBxBwqCp|z%B2_~Xl*M8!#-SV$-7-=|t7;uaei}TcC?23kCaj;8* zW-8zJP(N;EFE;@n+*f7r0jojFgsa^&YEQ~FI*i3MwRWO_Skw zf@YlZ>;qe5qtys)xi|%ywB>%5sokh@6=#rFWn~*eOSP%|ZKCn&o=Vdf+Y#I!GjWdb zVwl2M9-+JXok1Y!bnzb83=H4HQs$ysDU11C2O3X3O%Po2%UuGhfQ+5A6qSO5IXD5D z{S>`qR^>GbnueP8V=EtE6@XUdpb)vOG3RjcuFA5XG`hrQ2C<&_uQQ@~#Y*{VW^D?r ze0o~Ukal88E9h6!T-`TmD*ED1a8P0Rp}E3J`qMsHMuus4ar>zm6s|t+OJU*#+Ld0A zmwpw7+dNa)@RptMPhm1d$;yD5y~!z1(@@;xB;9ffXtT1>3Y|48moyJ~-GR}j z&(~-swiVik1BZ=>w}0|;vDaJPDFIwdo25nATc>j)L^Mi!aq;X~v1sPAv3S-@pWM9i z`9(^{D@tD#`}B=L!-mJOt+$PlT;s;a2-$rF9`jOZ(Y?x-%Q{5mrKZ!H2M!q?Z~N$} zvGt_Mas3ap{91>(v?~1Bv%jH3>c0E#i{p30u}n+O%l7pISyDr*_R^{I0RtU`2eoxYU`^>m_frUhhpq~EBXI-Jr_@~J zD>(7Kbk9PHsXS|W?@B<;vDr}o@NoK~)|kISo3+nwiAU!&$BG6t5Irk0zobM9zric8 z3sWIE3WHm7Lo+0C=0*QzIu`YKE=4-wI{=K%Lnho{KAJG;JBr;UBN9%0y#S)|!^V}j ztJyUQbfUlj9RMNL60g8Vy7a@aMxYmzDcnY59OI~s(7ItChr7g7X^xX%VOC) z+I48r`BIvCX&TpSM%<#l3J?YW*SJ#qgjTs(r6uGY0oqba(Lk!|XkgJc^eweM0a`@| z(F0(W<<1!R5BV|x+jBjjLQeGuZq3fESNK^MIJ$zP^sureEAX zAj?@l{NWE*MWuN`ef;Ae_g-x5-PYA+r=4~RPJI6Mx4&JvH5z>EFUSRGvN@Sm+V74- zH!$_mpy@wuLd?A2yV1&0Vws_1w~qIH`CGA*v=Y%81V~(?JTsquGN#=1=a_QO?Xh^~ zj99&FnbU9Vr~i;)F=qS@vD3Z>#4ZOM=mS~MZlvs7GO=z6tpX>RKEBfGe+ihpfM8L$+qjP_+G8aJNQ%^k=4?OTd zyzz~1>;$yaKgJ7GJWg7Tn)=37zq&S-nl~i1$f#(3eOaDB*1(t1DWuDfuu(b0LFp~L zlTWgS#8DT4u#&op2&H67p_D`xbX7)CEVrnc&71=Bm$$|OT`L+&KA^(wf~;Aj;0XA` zaxL+U6;8>dIw#Ad_0oGLLHc7;_%xd65F%d7AcW)e*Y_63^LmgU-+gb9K6XNe94LdCf#|uICe2$76l}y$zx_qZiip(s_^XEck5n% ziw^5prGrom=4qCS?SlgJj`x0l3QKK8fUnd%G#IAYE*gx0UhI9SS*s_l<=(rTtX@uN zO#)TZpqD(n9{{}o91XN7txrNGOhq-Wn$x{R`MdAVUXa?8QnXBMPe2~PS%6y6x1b$b zh^eKhkF^d{oZ=bxe4qwog?+?lun&jUc)JmW8F1{A56Z`sdaZ$GNh@>*4DPMX6KNOk zajEoZOZGTJKf$P88#g4TC#D?Xe(63XkXp>-Z{{WzG%Lq>WR>iW8!x|yrpRi$<~a_j zK1IDtKVuB_G?1wDgtyR<0r2@i-lEk23WLmu^Et^kp=q1u|UlW!1=y9^lg1B1vk8ZD<_>!Qjo z1g5l=jwA$g*-%E!yrLB7EIquUx2hGsxl(|WZl8Vj*{Z1?01F4SbeH+y2R|75?z?ZN z+j9c)_19nDDQt7_+Rb^BX*V2Sgi zx7{4S`1XKn{J3z z%a`SnXlaS2@IvZ#!;7j>ds^=LhBw90U;pm_n8dUwA16SsSvWW9B*=6${xT{_dF%I` z9NT^5-=mjKr7q2N*IgG!AANLu_OqWgFxFisEsq>IGA2xzP(D_-+f2>?1o;5*-uJ$D z9Wb#;8l~(>dH(t52jj|RmtD5b*ir!%kNZ%(?|tuyVZ++#UWyC|l#)LzfLLT#L?sSk zgUBMDv>4J6?sip>qGgJ-MR_o4mWNHDWvNbF``HXBFm^~?>_4GCPI*J$IODwo;`|Q| zic=5iAG>ecr!ZNHzJnZWHuCtU37g(u9`6)m9uqIZbL|P!$*)6; zxSl-fvA14%unHK20X~6=c2_{f1P?2kiUs(^0U{aJE43EoXIU6L2hPm~;fV=hv@hijqTKiB zgCcY(qYzHo)VxYJVO47wL-;Rj*R2>=cn{{gFzIEjV1+>o#wX&Cr2&_gidiSRE5z zvv<7f-@h7t)lQg(nbTv|#b@gcB@sg}u8szws7(^{*moWsJALT~F=&_9RHEUnYp%J* zz~U*VoD$bwd#z72t|aIJ2ZGoxyY!6B;5&keXeN#tHL4Q@N-&WVvv0idM$_tA0kfA~ zcKJFQmK9J@;QadqOb*upH0v0(O{rZ%TnPZug0ve#iG_B#9cBTg5J1oZQ@C>>pjuR+ z6uCf$ijcL~fdn>hfSrgA7qRN$>`O%tF8Kg6qMqIvI436kk!F#5$ zCu-BZ#8z9$GIH@uJE#bKMOSF6`lG#=G=gTLkHQ*tl6FUc5n4j)c2Kq>jWsqBr^{)I zGPwsdv|ZdE-fa?~h&F1qnv^N%hmtQKswzcaY?moiNgLVfd>zBfnCh%MmGSG*k6(Gu zx&FK+Gaf7JDgJ^!3Lq_Ev_GICU+8==l3t#5eR>;!GU%xE){2wOslhC>ErG<;oD=UV zJIkL*SJ~H3e3qvS=X+YLXfRBy7>&zU#ABcSq7Gu*viDP5{@_C~OW-P?t6IhMgND$k z(EYmVQK?4FzLByZ)7H9AhgF5aL(|Ok19L?W55@f zf>+X0`|Sz})Q=c3;_F>;qs{pq2OoTJ?6uckRaM1>3l~-n%GeYS{`ljM#~yp^5fdj) z+!V#V;$`&J(pr{X4jMG*1rLLGr5=P6ZP#e8z>X(QFVoUZmIxnq*kP~KZM(GG6)RT6 zcfRwTSh{rSI`@6cM?W2Jc+WBMy!I!#=|?|^-~Q-(+9UC~b<%CBfK^ME#I*bGmbT*c zG3&|4WA%#V+B-i_-bY+9Xl;-$wg4zW(s&GiED;kS!}y*d;$2yf@u+Q~c^zzY6*afY#WtV>|5; z*tx__=UcRBk?+0fN_^=h0qk)~GkZ*OI1!qY9SvY2nyeRHbWz-J!ws6yCD7j>tx;&l zSLq#g*g@Ke?Z6>zHFW5Z*kQsBj-NjL8TTm*6fCJK5E}18Wb{Vut1sC8GEO{zjMFJh zWpT(4dFy(iiK=BW;t7N%_G9d&Fz?GKx8rRqz4CnchyX}Ix4u0)R;A|Nf+#SkUtLTb z-8+ujb71Vgo%Vf|z&m|mBa*MEfEFSEwA>m4n7U~25HCr*Zqrmn?x;&!iwBjuS?rV| zdepHGsFq#HPx?CBDo013_7XVRtG6bf+N;ehD>h^~^g)vjh|x7raSP(UmsZusXWt)7 zS1r&oKLOghG!@sfKAkSEVtR|$dW%rE#v_m&Ql_G+weq)iz_J z$H&69@1;G{E0KEMl#62JlIa4nQp>H>E^M+t!K&0Q1WZiu%Yc}HG2Z_IA0z{CIStZzY(B4ai$+jzUXO&-5*c0So{=d9U;8hQ81b zT6!s}=xMrYtb+$x3O#1W_vzrgA!QNWZ>wIa7L}`9#rMbbGraq1i$ybT*Gc3{1fr_f ziGFQu(s*x`yGa#p?W5O&Htj;#+EufnR&%ztoR%za?C zKqVcbqIY1k-i0k1e_CWWmwt^)gaf)_14Ct6-kE?`XadMNM)1LY0;)`MgD%NRBo?-j%Q+lAp4WP z!n}8{Y`PE)rtE3sS7W*r9q3>}Yo-e+fGklnH46OQ6kuTg-uJ#2d+)t>3?DxH?=G(^ z3SfV>hK7c&xYc^^aq`J02O4|;Or^QviYwx&Q zm;&U%UmCf&7t2rEB-CoUqR9@zXqinSjQj|ul?#waf^f~$C;$ca7}wX=vHK5>%kEkd zH$Sq};6%=1lt8S)WEnn!S79drNCabk%wB{M{WHe=*k20n$+I;G1r>!e*PYgc=l8Gjki` z8^QX?aDm(c4&8PlG~jM7wWcgRhd!wAEIrQfNUFz+G5Iclvs8EFlhSzbhbktB`zRki z{SHw17C{*JtJ+YJbt{m4uDxBYjfPh=sZ)FB`%(e$q6+XqEJbqCwu(8DZUVNnqDXAw zS7t?yQ(Cc)05J7u*<#B=TaIw?vxp0H3#bT4*{b(6E#ymW0PD`V`nnjp*Pb!_z}G8g z%Q7=`;`N$&^F^sbt5DUa8BHJ2zeW8wwH4JC3a8Jf7AWf|vduCq(-|mKixEqBSq@#B zCD5=rz#;;D$qNZCsxTSFIGk4#pKKjM|4|;|YNl%`@XE|!yS5IsyY&zX03cm_@x}4U zPkyq8JkiDV%s-+3mmGNC^H>_9|Pu+coFYb#*8W#q^gsY=r>GQ&y zuZwX<|MNQaD$}uarA5o8`;Oi^w)w~7brSSDV%bCY#^T#{=Lwe^%?U(fYgx#KYfDXP(iBN8 zkk_;mMVy*>K?=ZAjAtJ6;3md3{ek9~xAbd1g$0$eyv%6Cbqu4Z9%xoRpC>#s`S&DM zwgD-<&b|Cuv(?=D4|z*zgen*uBV7HG*S?QNDm$j=V?e^Rf<6g!H3KT@g#fT+@KWwJ zfnW(FnzZiQlnEGD&4xW7p=Sb$h3$GlGl2u5SuTH;y8|7eD_V?bCPE`P3Qhw&gieTC z=0CtiF;{(=7dZ+L(!gQT()ZCl4N3w`s`254@6IOZqh){@~{=!G3lG& zBV~!SGPEPDkY&LH<-K4RFLlR3xYE<6v@`vJbWW`}_Y?VRrt2u+`BB&1u6C!*odQ4k z$xk|I+->gl{d?>8rZ>GQ*iWvbL1Xd43oq;#RF!T{>0R3#dg8}p{U<7Ym_aip&kAc*1U7Mg z7U)~GM#~ph%{7<^3$c}St19N@%VqtU)I*!{D(D4G+c*sxdT7?)qy*3uI+s8!JIFv& zYmcfN%1Dhw*_HCdWpYDH(-%79@2NC>QO%uw(I>5wp1xHYRW`$mcZ1@PPuf$oyUHw8 zW0#Na#!FB6&Z9-)F%LtEO06_Lyyb`fvc*eM5f7c94ZxO{%EBO`l-R@I6Fvio3g@qq zj`YM~7w!`DavH@|z%u1?+9G}PvK2Ao^fTq#GR*Qj?uz*rT~^^y=%ubGs?Z;CBONE5 zD|8~BM1YCvtfVRF*%5$b3^n~LWfbMAJhHod?qO0ZK!$d<-}jbFHpa2U6@CZSe!7eT z?UHPl-LiJ0&6xsdL9z6?+yiZMuIr|#8&2y4m)dS)#u|$P7J2*i-*!T5edGsY z@_(EaJAdnf*y_C}>kDV+<;YiGeRZ60!U;alIB}xE zM3!`~t(gdaJpJ_31`|2moc%=CG0>N*A}rJe2S8nQ)m3rDl~=|b?Z4U404k0fw{3jz zm=DAU-uvzt!RgP+Yk@=!z$6b-3L9w`LVC_tERYaR%T~ZdgnMVT_~bGXS6LoiXUW&a z)_!UfST6;RJz#8n`{*5GtD*fgQOXH%!A?q_T8fgo#p-g4hv8SvlFa*r+3gJ39|$VP zo>H4^K%YE7YP}Co3#?I~pa5f=4`qR$U^7NKW`HtwE0D1c4WINo!tt3V)0qc5g;qS> z>Of=7pc8S|yBFw1}+lT%GL)dv%_O1nj2=rmcA2Dj+JAuhJ@lSh{|=7=lPl(xf) zaMI!z6~Hy<%BDHbQpE>|tXVnN^n=_&e{K~RGYVH(!PoF&N*MS zhh9ZnK&Qgr4kjk8a(pP#iegFsg3clpG%V?Xbs}!8E zb7DhJK!8TWtcWL5&D1EcAqt3Pn5Ua-G=AA&!*oV!h)ZBQQ<0LZ-N1D{*{soCf3A)m z5)d!}eELjrF^tQdpqDfdn+P zaoGhep(FkZEk$PqC4G4?JUz{H?lK#GI?Ek8_Cw>hK@zS7h8WhDNZxCpkM*~9B&~)#<|KI;#HP>3*4Rkl% zz1P}3YwtO$YK&2%YK*E`bJSQh=foN4U|EKx_5D%E{99*`azFJ> zI3YA-OdGc%r2J`2ifNo#q46WRg5}2qU^-L{PB1&d#28-Sse^QQk={^l`P>)E>WeQC zboPc7e{x#6Z^cUK%_@)ntc9AF=!{$N)?dY0SMD?6(Wx$E>)-UE)cChv-H#%u!MxyC z`A`dg>QEo_qtnpn=z_~AHw^2LePU;5pm!+l3@wjg9jo<~rX3l04AK{DDEi72P$nPq=<=QJ7XkueN$a2H~cz?P6W2cw( zci&a!E?S&Npt*St1)Pmj^B0sUJ>EQL?$GOMb;Fy!Z>^n+^Y_}j?Ef3jFNeSLjI#ep zui#DO=73Z2{qKLjjA@%jLtM8l`S6E7eA^0cWhtDAhaGlU!I{XI-g8gw5*!LR6R*7T zO7Bd3KxZOlHr8h~K(QCtsrac+pQVqmd`=F=n}%wLBj(_P4wO^zx3yF8H_JZtBT@bZ zTSiRVsfeM%;c75Oq-&05uh@{$ipemQYq1jQmpBzk_o3IomS|wVy%v=J{<5RWvICZQ zLF{oS$07^L<|xcyY=*N&AVm5fHqNzfQcxMi?Y)aj7mt4EXiM7D)9hLrVDaTUl%~CW zrCIVbnJRd3U(+n~Jm^fUWTr{Tl+2%DhK$)sZM73i({tWs7$-vK?2~A59)Pd3isK9e zr-L1h;sZYAl5gcuQ*esO=s$~P?h7oG;hVDH(0bMUm?>+^n^VP%Zmkz&kDQj(@Yt{uCcZq@DV z_%=P}e9x6vTPLm?J)k!!py>&?cXs;vBqztX|AuJ=Mt}5E(>nt}Kd5T6a;MKuuOc2! z$39YTxDOg;m}bC(7W83eG+@(oW~%OMokrbwofR=Az6ZPVsm7n{cQ5ENr%Vx-uf03_pOB~HapomD<7r{YaF-E7i!;(9vhzyr$*o_|t#{z-Bw z?yYw!QVt$UV4x7lw_mkM17Q{8orpGIWyH`98>iL0B&|cHkt`{#mio|Z-~lwyjVsYC z*==rl@5>%jo_X|snj+?EXfY0DhH1RAGm(WhPC|dtx$)FkoeQnEi;>6d-n=m546Fv zv#%MhDd!QrO3)mFaaKtKq5hoiAMAMAv!H1t_`YPN*N&WSp63gA5T zLI+-_I5a3VW?^%e$L3&`C2}w<{jn{NAx2RSk5_CJgtpDnFk>e3Rx}6O4Jq8=7F;=v z17P&2gEAIZ89VJmT+39PNO2A_M^VoD(kMRX`a%ilBbriLItNB* zIvQXrUWiJE{#+W}`Y(N!;^;d}?C4dxvvDY2>ur5Y8A3;sY=7R#8W`F(vy*G?tD8{` z?2HSKFFqfC{PF%`@r(*PA(gYU>y%SYDS!H>f7-Q}oqq3o-}}nxr=PCPYm;-i;!JEe z1ccf6i^-jU6P{Re&1L1fGyZFtzxPtT5Ao14e<@#m(@V#1TUB`9;(fQt(Iw~OyuBY@ z_ImQu%YMJ{g0kq)N9NJzA=&L%l=Z#evz}M(x$cT`^A|p;mzS?AU-`;c%F#z3Jw&-} z$=!F~ecK9dMXC96gdTb1k>%Ha{nt0^Ok~&P$3FJ4vPI6sS}(Rs-qC^)O3wC(jXK5` zk366G=}-Id(34JlUK!qriBs{w0}d#^e(W>K*S>yXS#>*KxyAuAUFKFp)Xo^e958I; zAoLfXZOmYpV#s7iA}6AL%WXPj(?x%HLzI?Ft0JE3CZKneq3H@J~S04c19BT zxW^c0^)N#)gcc_^F6W5H9FI)8ZZw6n`m~QBqI=gI8ptj>XxTNR+q3|bN47XIBqL>j z*a^YTI%v3F1E(DPhaFy&$61@}SM;!)^kBIqs)UJd2lAs!?!^t>=~^-a3p{unjQ(1& z;M~vATf2plrn{oQ^ab@fUpvBCKwTKaakyzCqpYsYHNkZy0lgNha&l4H(cg(BM3f+H zp^2O}S4HJI{t>&A?Wf-J}jlj(wY`N>J-zs-}?jOruC;lhp zW=FW|{4ZNJDKZKf5o=hhH7y0XuXK?sWomT8;ehU1l7!g?LO-r^IS4>#CNK-^Ab~uQ z*KtEf2*{s1rL|A-@R}1&eb~7g=vPBKSKqr-ZEN+#+(~{9;7oKDUwl61m}5q&KKI;n z%jZA;`4QN-XV&wQqFpH5ciXLH^^YztxBvV3I{vw={^3Ps?aJ%RhPC$=oP{_SZ#nm@ za>bv&q1^VZe=T!&(^px2ryNp}Zj>A3MBMjTCzUBZMoswkx4%6^u3!oWFy>jf#(B0(t=sW!iGbOBH&JAT;p711Xn}bF{X8I z%59P2cV!~hVP2Rkp?Qr(nit=s3Cv{-wdzXr80$tG1YW|C81sv%-l%U-U*}|Oyy7Vf;RK!$5jR1!7M)pT=oDQb zKL;m==sGZTSNjBAax1k?1rq@@=3(Ndb9h19?!)9EZu(BWwX*Sw5OoWJoz6JdBv(dG zbiq{?n3yz$;w(W^@D^+ftJs28%m7J~X{hQZZ^Vmy^gY4-Zs|1^7(5~hCJb-{8tyFj zPaWRTKcIp~9FtYk;=$LU?KPy1S{9n&dofw^X#;ekZ0#3(ct*VK?>|}AUU8K<&4$xH zZ+Ml&7}eom;0Zn=zckVG1RTA2oJK=r0s6-TCkL7%)oP&}e0&%#jmy#4c)L3&-wME2 zfh;@l`!KT_=&jJ1)&H!oo%aBKMqgy@!^ab zVN(sPy?I5s=6!!ue)_+8H>Pf!!|zrU1n1Rm`|ej3FMo^+;~*P;bbM2eTLxL6_c-q8 zz?a>B_`@F-zEJ+r^6Q7h*@$7T_ z?p6%ITW-0veEys-l#iczR=H6xl^z&FFqNTV10=>PH$uMHJl!0GN>8y{V47D$umTd! zPDiEL#pt|niu=%O;DI!t*X|2FG*~M!PQ^cdfxdjdQ14h}2L$thVs`O(uzHYVZmT>C z>B@zjw=u8>hPW3xm2V&Q8rZA`SfDewx}oTi<wLzd&KR9-iX=DSn|X7P zACJ`f`|iqhB3XUCu16Exp(e?XgrCPZ`sjG zCM8$p8PZA90TTE}Gxc+HG9a>}kn5@9&>Uk(FAMS0TMeFjDD#}~(v%|eg16Pz66DNY zr>amKlQ?>98-f!&L(YVhgjvXi@aQ&5YVeduE1gxxs&+OPduS+Zow#^ABd^DfAp;lvtS z1w25*rtiI{-0+cim!Ev}-DUkrXUml$Mz{E0q3z&(P<&rV*g62GTF(<&P3sKY|!KE zedsmtKpM!Ht!Az>oC1CLo(s!!|F1_VpG73gqd9u)Syr?U8_~9$RbDZ9ugjelh!HAQ zYwo+}ftt|cdJRMaIJa=%Xv}ah$1(4DUhC#WKlo>QFB@5rceyMivzk+S%vKQV*4`mm z5Ukxda(w{F;Thf1aNV$&Z95k0vR0zy+6x|5I5Av=jP=#3Xb4mUNv=0vS-ao$R7ak|8M6z2aO3*zu4C|wfQ)vh- zIZ%WU9agwt{y)Ay9lQi>XB##45aPx z0i_-NRrg`-HK!sUW%}@kKU}uOnFv1{Fc=!L2xH8|d}u~Q%lQ{&(edl@^*7vDKK}7D z%U^%!Bifnx>2kx3KXudc7%3%mV0AbZ$6{RpX@0^;#ZbYZCrvEpA&Wv5-gRdpyGXrr z5uA50QmPNV2JT-2b;xyNuO)y#>xl=IqYm9$V;pk~$D($N)m@13*vP<+sjcytMG}ki zAro8<#fSJBf%|WIkC`nR@YJXA#X_t)5}8-+Ttr5}yr5H(aY;HM(By&v-tvvA63=jy z)-cmEgLAzQ>~wHO4hTEM1QZUCuPr+b4F{NhVEmExp;Obj^$NTpm3A-< zCvxL8^3uTjJG;Rn$n3;JQ3}>W#vf_hJ43NWRo@D9L# z`?r7FHHF_osqeYx9)Em=cTcv!q{-Kxmo8n}Kob$OoANo&dCo+%btW5muRg$X&42&H za`~U1Ty8q|v*ljiyQmv$UXWi3ALZeAsNp9+`N>0Nu~p@9D*pJ#KQ90DPybXNiZjzj zMKcvJJ`@5=b8O2sxIj1paVq}PdFN}#;tC*GDmu$>$4N*5{?T!z4_AyOV3khJ!@MU^ zjzmA!wJv!%5$$Benb?jrr9bx?xL*xmDtCeLnnBlwV|n?rj`X9?ER5{dv4bOyM2LaQ zL?`9gyx0~cUVbE~UISeneWKUEMjF6zSl4SFeqMC>3mTahnOC9lZz-Yhcw^M@$2hFR zw(?O=FdKN6t|-GkH_jj-jtOBCdDh&S}W5w+2^%z&&KG zSFTqdloKKN{DrNZCay?KpjEWb%U)RaIPO@bW?z`veMvd!58rG#kq-Ioc(v@+B0)!0 zZ@ynR&rAtij$X}$tq-8fJw<#(=Us^Kl#LT7B&8IjW0wafK!qA777nHW*#JP5AI^rd zKkp0;OwMOxcZN3lI(C=_*lEQFTAujCCzieT+G~f^Gs%oU{_&5OuYdjPBO;u1(n;mG z`n~0R)-{P32;%XXVzuU_^ zVsx7uJfQKZro%>O0{StfbFM-s=v~D8DxDi-vU-Y9M|%?}NV?|9O3ub-zlSX>yDywu z_S%$k6t3W#5mEe~j4J^K92CmdY9c;S^9dZMwRtuf+JseaJj zWsSRLj5Bs%l>S_O@7-qCpLz}4p9Zq)T4P2NhZlrd*=Ziru=Hf2vE8VqZh8H`(#syJ#1^Cb}uu6pSyZAmM!BVAu{XpCNIfm}JbZLgO8L?B&I8(D|odKodpiB1S+T* z^HpCkA9BBTOTxcVk99mOY$qK+WRlznt=MTng8@4fz*o74pp%#6nvyITK#{OX*5u>5 z!~Q`5^f!5SC9ab>=CW3o^rjy~reuS`3m-qGTku@BNfXC`C78w{e7Jks`a-ATgNsWK8o2X15p#A~+MwLRBbJw?zxyI%X6tFO9(z9Ujjt}(fADW4v-+aypjxh-?xyG` zDFF*OG;#tv9es1JVOQch^=Wh%dh1to`mUAx+s#MiUh|sQ_{K1K672jIufKbBx${51Ia2XC=bTfX``qW2`Fa$3Tf#TL z`OR|KWtVMB(HSi>Fa}~cu!iYEH72k=Y(RDhYzi$nrsfGF<3j(~>QizI>UUBZ?l=fB zI>bx}3`qjJ4g-}LBm=~BQ+#|yCX=0rRIWqx8wA7fwVfh%-@o)qDo~y3A3d^ehvHoN z3P+;q6mE+4$dEOz#V|t-;j8&82W75xo2-^m^(bzJw%efcG^L4=jPeRm3rO1O$S@mb zLJ|)L=m8CMophK(*$5h4N)NNK{sq{?0SaI17h_KnxN)>id3FhSQL4O&2C;Ych!$Nv z?&p@2NA9<%9CP>{O7HkDl}Ebmdmhnv%|r{F2jQ8&aB%t8i?1td*4`)C<;3%G|DEHE)pH8QYk`M?dDjbO9tp=Ss6$q#~%?<&NF^a`m zREb^4&TbWAe5O`KVdi=mm9EDyR|3I!2lTN~$e2U2C@8i`WMka;KBzMM5DG=5+K9^p zX4^H@1_^mPRMow}s=VC!x<{KOJ-dq;gXjSZY@LTww1SR2I26ydQHQd|%^3rb4Y zsSz4dZ5*V}l@|@;hL9h}oa|sV*GD^81HD6W2P?Q|KdJ$~0DJ7Q$CgJw`q86^T{y>$ z^7PYBFSp-*`-m3!+Viqy%SK?6mU+fAo?&MgkG%I`+cdyOdcN|Nuasv!>sedPfE|uF zs+;k;)eL5^0By#Qz<7wouu)@TKVV_lmJn%;t1)6rBxP9mdi5(_gPn?)F3`$Eu~Sbt zEvnfWW@`~=BJWKk@5Q$AEW-H^OK16D9;rNai(T44dzF?IHO&Iub*db7@R+omjDB2N z7%v(GvzbL$0-z$sFPH++0Cs?Cz@3Ru) z4o0FD>1dP3;6`t>2T1B_P6qO=+=gl4DlKrF6)rIy8IjFqKQdN*Sa#r2==ZMqS^3_z zca?K4y0z^8u-)une9j@ph&XzRk4dvFmZ!1d|?=8-g6K-5{WBK77Kk%6FDTB(gSznUTDaFy2Y@p8(9`7a}eK1)m2b9o)DE?_z3cg<32~b<71>2`Y@{wBmyV zG#iE5ZbOw$`_sR`^1+cTls(^=Mx#lw=Q!&|c=(8b9V2njOqW$ZTj}yyoM{xXxTYM} z=QK`iW+7nkC)UAut$$uvSA8|r<(iLVED$L_lL3990*BhxmNiTnJ0>(Q#R-9KL(Y6- z&7uzjC5v!eVn~%AnS>)<4@z2pf`m;9G<=+5l;)W1ka2({RhFu2&tsm~B{k!P`(@LI zl8v73uyYLk>Si>p!N>1ser1E{qr>R2>WV@mIQlpBgkprc7MkH`vrXCz5YeO^FpM$o zjkZY8k41>AdHgppJ4?{J@soS<<)!Akdo>m;k4_mYR*YcCZ;gmDa}tiIwM=(nz;(xb zgX=IQsxDP#u6W^CWJg?_T%mOun9E|u0av}QoBAuP<~`>DqYl;C^3YN1A*a4s?-!hX z?@g3=n+=aV?H%PuuXAMZE~Oqx}`<22E8 z-YFWm^UgcV```b5J2|#>$JI`0=uWKU?6c146t69$(fY6zc^^MD>Q6E~hJm_>4hE5aEm`#fZT{g*b-zGmDBr4Ze_UA1kJ zNMIOLJn&&>qSD@t01dQMhU1ldJyza6T~^+4Z~5{M?kF#M>b~VaJ#p`{WH;!?Jy>Y> z11*idTbm^-03Aw|XFUF(@|EvhTkczTpL8xm-Me9hkPhN}6{#60uW^>Odkpcr*RGjB zhvE(8OF#W*%a4 z0${8fu2bO38wWBpp`#z9Zxjz46ohq>>B<0a6Az@7HazO};*43i&90XdK{5Tzbw%t8 zkTe%!%{$FAVh{*EN00x_0WaaF$m@eq-XY@rdM(bT_1L_~Q4Z}9soSNzFFOGMvv6aP z$aq65h5;W+r))5s6V$#+(;z!L&b%c7Xtg3zq-j~rxP{1#oDF7rFbIPVYkBuA`W2$R ztCS^e=4W#?4|G#=Dmxr9{)QownD#hCCkn`khK2)Uv5k)oUiCQm2u}K>vWdz?%y_hJ z(KG)(l7^=%6VPdxrx}BO>M9)Lga?*n8VOAz$)T9x-1EP#7pOxhc&dk5OZie|DCcYlU%hxJ%7W2Mnvnwu6KpcyPi671_ zoLbCoSP#U!$D5o_p?T zC){jtxE_4)!Tn?I!yUX0>Fck*zP$FeuPxvI{`a@2G#~hR?|a|tuPkp-fpI62VSsUf zkwWkJ~MHh!2Hb^xnFf444$k-H&10&If3p5y^(D*|uNkhX9#SI(Q*?7uF zTGq>u7loBdv0czhiJY8l7oqTdjw>z-_4IH>|%`ZPKH&=BZfev~8`* zG`iFqnQ4>cx-WF*1wY!=_?mZxO~Zds9vaK1c9dYs*b{sO;DW4K#ub+|iJH98$q7S}?CX<>wF3xX>~Ohbm?_PG|0;ytu76}EYaHoH557xRxa9_}0<_0YLYLLa^sPP0df^RU&3TGgK)WK^C79<`%0`(= zkmlet9F*6;7x$h#;WNMk9{~(dJ1s?X;+#pt+$NGb+z)DFvTAF+ZjcBZ&36os6zTGj zGjzTNp?4AZh?ua6zCbl(NGA4N#Xxrn(W7<1yf!T6fGFb@G&BT73^S8d{h>A`)ZRE*JKY6ZPlb_?oKIs$=yMy0@bc{V9)r=^OMqPaP2bsxNe@ zcl|`a(AQ1>qDANIOr#I!Z2Ck0^@zPgK356DCHFAj*D9SN^CT8~k13))>{t!ZcYWwJ zuv0a_1i&uEn{K*kr`9`em7xcAklk>@4I@o^)vI1r9{>2qkH989^Ze&OzdZfvPoH!# zX0-iRU3FD?jgRXP<+3{8#kaoTec;IYne49hh4mjv) zUGR+aZpfJ@m>qv=+Z;JH;{Xibz-e0?lFiA86VY^P&ouAsqg`&B9FB@N7}ofvEKW*0 zHo;3S=*ZiN3ICe69!a}S$t0axPj)Uw$I&fbqv-jkx2-9!{K!@1Uw-(r^xL?fJr>hQ z7Ev*hGm8CqX@xv)*;0*Hna3eZKj+>bW$e;mlH7ML*pG?l^$mx z^Oeg26W{14u(X8*mBxYa;a6!GBeiT-vH3D;T)9RNQzJQeR5&n!fVaL$id-N{sD#>3 z&*X=3$2k>-5KYjKFVrXb4oWFE@7SSyO|99m2!yj>DzN<=TA z%mAqAiH>T&H~k^G()=-Ak(&T|MBmI|6>asIt^%OfVF#M^4+jP5v6ab>B`#!m`m5Fv z>hU_)rf$-x&AOJ6!|UUVE)~Prc(E@7U>ek4uGL``XvaS!bO!(j49``LdV2Yy>tNGP~`zn?KO< z-S2+4+;!Jovq7X8k%7m?|Lo8H%-7B_#~f20``E`8cC@Zrx2{}s%{2x6e&;*ik&(~J zbM^(>j)+x_2Pc)rA!q~LA`75AD#(IFKr;T|iOh^f@10*5tT9+@xyTIh?#4XhEDXG0 zs#ph)(|ETWFsvMb^#n8iAF32y2wA3H22jMn5X{jBTf8eu#!lYZs6rG}o;Z&|*WX>! zcxnvZRZ$_$!B|2L^2DMcBBwY-$R!O9U=}TvuA*$L3-4mJ2)r9FScAk^Ym1zQhlu(p z+XA|zAKZ?*D4<67gWEv5%0avV6H<&wt3Pm@MK|I^|M+omwGzD>ug5v*M_H*Gru93x zlBb`$0uJTa$L!gfF?KQSv!1H5qF10n1f2X42klk%-hENIW3?Qs(o6J3pUdc$4yNT$ zWnB0@i;7vOvv9gu?@*kyu=j$!%fdMe%bE@MWGtZw9b7g(NNX^01g>!)C!igDfN&T} z0Xd?F?AFs5)HNg$5lOEekk07Tt{;*$EkeJZfR3RX4Cft$59_IuQ3`EvU`=G}jl7UN)@#w8!ktO|&hzY|Bho94V>rWrn5#EkObgKbRA2IHE4BE4V=)u$P+=7+u?p6FCWG*V; zYejkHCwMb%7(FdGEnJ&d8oZCUuLgRD;`Y^N&uuF-aORn3mJ?1mp*-nHPuhx7vxAg( zNxu8t@76~bnR*j=#QE)Se|yhBb%wr0>FaP2%{$f_iH50L1Z9dDZtwl+*DIHq5M7eOSkGkl7H;vI-G zG9bjz@S+^c5i>=*1YAxFo;h>loZt}~b`P){h*i7m3y5K(@;J!Ei$#NXo)Oa?IBQYi=6aFu{g7^tkP2CF*tuz$fl)4J8%=f5 z2HF9%GRPFh(E+g3101kwN$DLLiltlhW<5!%aEX&qJf%xS6sfDmsrbl;?^X`mZ#MzPKUghpW^~~( zrMz;6KKZEq%a^`$r=2|3o#6ah-;F=hMZax#+HI>R&!LF0%lA0ET(asCb*3_P3})bv z*MIZw)HK(c>4uz%8b}%oAfyw<1kS_^zA%A@Lq@EIpIA$?3?eF_JeL*f2gi6*hVjpC znf7H`Umsarh8=tvhsk+vAZouMBd#prJBR=f84LtC$nWnQ6sShJRo>jMv#e zqzLGC0k0PYc-P4n>WApVRI-^*MG$H30cWS6+STgkJ)JZ&^(8j%2Iw#TM{BX0ZMD>Tq1c>O|jTJe%gv4_cOln!PX?ijNv{l4L@6M^$r3zY~wM?a)$cT}F zBAUF88Q26x9bQ?hhPj_ID2-IrQ;h%#rm>}K%UV)j%;DKa*U?d#4q8`Is}rTKDWH=R zqKDD;qEDo0x)K&eSy{nF0f2CBi8;@Q8W-IhWFgYIw%Skuf9OVF_O98PBt3!0 zMp|cmpf@INIj!0?rn25mD4tkQ(Bq_v6OkQpgFHLya6E!7^rLzxc&3E>C^xQ%7L4E%S<3ykgcp&OPpZ^wO8U zw7ldcFB$h{K4AMggFr^X9Cq5^Rj`2&gCQmZMktR9;Y_G8??A*cWy4cuj2=sFCPvH; zOc(-C%G+#l8t08+E-%wnu?S?@h(C`vQxE*z7GO$q2*xAOybF-}ctjWa>}(_t9Wdej zIJ9Ewa$0zJ3g5k}kw=!kDF>YQB8Go>Qofd0lR;o5r=p>bW%K<_+{O4l}sCyzkqk?qhn z9*&_?oqnexGIVkyYxJcGqEE%AopVE3eJ|$e_=h+U8F?B{`X_~P-2Rqpmm<#REF&?m zWvY?K*(9e?oQTMY`HUGir(AdYjQ}Pt9Jc%Np*FLSwn}p1dKVVwk9Qo>y@Itzo&yJ$ zLhE@4qNfL7BK4!o>{iyHl}+%%vC64<==^B0%1+nErL}VDXuR7S_P|tktJ7WQJZ-A8 zi1M`0bvq3XkWK`m;ln_BFNbkDxt>brvMw)OSQ+7gKIr|%nzGgdFy^RQ27<_70vhA` zXh?HpyFOi0EYU@r#hU8McpS7DmQ~b@L(@bEng-9A4wivNNLg{(s%2_c+84NW1Ayi> zr?RD&?vc7KPKnzAhv-ygD5gLE;}jDeIF~a|Sew&GnUtKwGg^&itpmx3n8=s_E8(Nim>*g8%geKyY4C3$qqU)Wpcw4sf|T5^1%rjn5sZ4MY%H*Q34NR_t< zY`s=o##aFPKn1_X)d>3u%G9o}Egs6zr|xIuQ>ZGlJx4}(4+jQ<=3;hKbLaD2k}<@* zrHQxgG@LICkKB4EqUaop&Q3+~h?5cg+&B&!j@XVyoQ>dlpI<5aANOnJfRmm}%lc3? zu;&wwEYu->7`k7D$>EFR^% zv7oYK35*3_OLg4XDym4X8ci+9d<7lKDFgrJalWt{*?GO~aIm(ou1OE!|9HPpH7DpiI)JfWo zH+Tz%BhF4YJN!oZ+Bu1H4jMcA!UrcNACuwzg?8SxShTcby_|nI3vmce3pVZdKh}@3 zkjDYYQQw1z6VmUAv?H-O458y4jNy|H!hpl+7^mT=vsHAKlQ!w7Y3r{#Z%r?mSKfYa zIrAI0xaY_6ES5l{S$|R;L+F z9qb_H|qFm<7_aOHL5<| z9vT=}Dcgfv&!gACHfZ3l|N5`Xb=O_D4J9Y92)i-g_rCWPc4xN0g!k6BzO@{1zyS?3 ztK(n!m0y|o9dqN}L5wBdPkP#Eri811DeVg zx?u-n+okB8m7?+PMmY}K2VSzfFpfjR#M6#RKFFea?HDAsQ?WS_2M)tH7#ppeg)Sp} zE!!N64URL>PDkOpHdUFeBkGqt?Ku0QTgULr&kUl0sfFoF7{oOvFqpQedfc*oHNIF# zX_2JIm)|2zW``t+u zkE4&gzw(S<0>_^hn1R{32#lSIT$=>gD4CC+)M^{Q+J@GYI1VYKo~bmX!3V~CkpA?f z(WNton5y+_D*~fhq+{A9Cg8&dWrl<6O)0-SOuYQ31~sRne*_T|KMt)p`V&imoVX-Wj}=yRI#eHZ4Rl}2PFTbkbBF7Ue$bWmh2TnBFIA?c)zJ;1L)53p!*^o{_{{!__>09?nB(wEIm5I~AF)aZa&Y5jw*S6OC9jXQE?` zV;;HX&T5C2coFlizc|GNmxFS*J$EmU{fqxr=IuT^ew0U*+dCZjM{g-h^syRrj5F4H zq+gUr#~e5j9V@*+Iv&-1Ek~c}m@C`HJ5Lq29ij{g#shtivyqtly@SUHkjzxJ+k^2q zVnDGGfpO-srZ|JF5%ux*(7^CKwmtauJbDdmwFdawGhaLI4F0X=FgXQyZzLaM84kxC zcU*bavz|2!n)T^7yx|RH*|KG`Ue28@;9h&}RZc$nYYZIk(a zwaw}e;{k(!KvZoC3{a9WPL;Nif#G3bXiYAdfWR9Y7T7r72yqS)cZ~5q7*1tb1PJFv zISca~DTfmgJ{7M8dJG@RvslMj2))vF09GfUciWMsJkCEHeUuF?^=B8PVwJQr5oemp z!OJ+&=dtWshPp(PN5r8MygJ}0ALl_FjZT9T59+s*P_f%02VQePIxpvD9D_Iz;}ndu zFS`tJ2Iecxw2z&LI1J-loW75I=mt(i;pBjfGqE`tabl`o9Fw%Q?q0;f=(=c|9iGu? z(-$&+a?Exqdi>F69(2jIfroia+ObB4cjo|8IB4k-=iBZ?MzYg%uMBOE;Oba(0l|K9 z`wAx~?ey5aA8UDX#kK2BATFcsZ&WI-!|qyy)Ol$|dSIwi*3U%>ZjCxG(C{LpmT^5w zx46-}q|0%##vRu#61lJLz;s@^XlZ%een*!h_CB)gvv6ryG;fjmFf{Hmc(h5!U8s}# zP>nW^jdA2n4w?C6kxe>Ih7>hT8gDx1%`$vpZpREVlCA=D&ROG)fKZ8e$!fDYow>=B zZERcIiYlJzK$9$MloDAxxrG#gbqQf@zsC{60m1=I%;E8oan{n%_=**O_=w05J8407 z`UxL6na;S$JvAAkE6Lq<%~id1k~KY6-9?j4=Ug$m9)xrENOei45NGp8hdwCH#hieq z-7&3jg^yk2hCW!U7(dZVu0s_PAo3$;)lpO*&9oRiZ^)z1_2I2M&w z-n$ePFZ<6gDhvDfCekbS8_>^DZ+}a67)sA}Eb2nFb5W(5GqE`v+4bpuQGWtkfb-D~ zNcFJ>h5JDx0Y1=r*J+KpGkGkKS6z%3=M_7xvv2>xJBY^D5`(QCI3)Y9{WU;;^`Y0m zjBDWRv(GNaAAfu~_Sj=*ye&Jb0asmhRXO8~Ge+3&v(G->ow)HY;Uh$|Ftc>&((-%1 z_j^8Op?EVQ5{~IpPC2C@x@@uVJGJ!Zwif`tCzEvy}nDnpBG`?!9WGTsgd<0%BQL<9XqR!X?3B8r$<&>7pw z0F9WikXC`YOrEOh_Y>Yz!r+@uU4R~svwTBRCqhP|*J?4cYQ~>_|(4K% z*RD>p(TA@svdD^zj3(klIy*FQrE1_>B%`7&((Qz!yq=TxJy&U~ULY1FEtrH1%moe? zVp0U1-A0nqJ=d-Nk3al`a?HWUm}2;}>Yi0))jhYCHS6vvtL|N;`0jF>;#K$XL(hhC zYsGhEXaGSx-POvwO{xo7ERUO@alv)twySO6O_}0OT0!Io<=R3FsP({e;}I2*gV-x@ zt|>RbK%d+_K_x$Q9#G9hDjJ}<$P`_rp@!8qTV+G-@8ncp`Z4+xw@#roy%;tf!z*7c z=4vGeGuNP)5awWKL&!B0SxDP)UvFZO9-NMDyj2Yd*&NrNtE)21&r>wzqSxp%Y#_7V zJH3jgre}ptJydc%BWps&&#-gax&~SGN`B-TuO%lc@-Xtr&1PIMpEQW`%K3hW$)z;C z&(1-{(p=_SON@ndpIZ+4o!@dB`_lt!V6R^|x*Vp*pRf2IAGaR;bGW*RVVj@N`O}Qv+eB~?UAOGj@1(M7yf_efjO3J|qLihK+9qDqov%Gpi%lcLSv uq4~@sG*2kshF=U7}AI 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 L# + pathToConfig.endsWith(ext) + ) + ? pathToConfig + : dirname(pathToConfig); + + return { + ...nxBaseCypressPreset(pathToConfig), + specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}', + devServer: { + ...({ framework: 'react', bundler: 'vite' } as const), + viteConfig: async () => { + const viteConfigPath = findViteConfig(normalizedProjectRootPath); + + const { mergeConfig, loadConfigFromFile, searchForWorkspaceRoot } = + await import('vite'); + + const resolved = await loadConfigFromFile( + { + mode: 'watch', + command: 'serve', + }, + viteConfigPath + ); + return mergeConfig(resolved.config, { + server: { + fs: { + allow: [ + searchForWorkspaceRoot(normalizedProjectRootPath), + workspaceRoot, + joinPathFragments(workspaceRoot, 'node_modules/vite'), + ], + }, + }, + }); + }, + }, + }; +} + +function findViteConfig(projectRootFullPath: string): string { + const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts']; + + for (const ext of allowsExt) { + if (existsSync(join(projectRootFullPath, `vite.config.${ext}`))) { + return join(projectRootFullPath, `vite.config.${ext}`); + } + } +} diff --git a/packages/remix/project.json b/packages/remix/project.json new file mode 100644 index 00000000000000..5b3e8a529ce2d8 --- /dev/null +++ b/packages/remix/project.json @@ -0,0 +1,65 @@ +{ + "name": "remix", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/remix/src", + "projectType": "library", + "targets": { + "build": { + "command": "node ./scripts/copy-readme.js remix", + "outputs": ["{workspaceRoot}/build/packages/remix"] + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/remix/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/packages/remix"], + "options": { + "jestConfig": "packages/remix/jest.config.ts", + "passWithNoTests": true + } + }, + "build-base": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "build/packages/remix", + "tsConfig": "packages/remix/tsconfig.lib.json", + "packageJson": "packages/remix/package.json", + "main": "packages/remix/index.ts", + "generateExportsField": true, + "additionalEntryPoints": [ + "{projectRoot}/{executors,generators,migrations}.json" + ], + "assets": [ + "packages/remix/*.md", + { + "input": "./packages/remix/src", + "glob": "**/!(*.ts)", + "output": "./src" + }, + { + "input": "./packages/remix/src", + "glob": "**/*.d.ts", + "output": "./src" + }, + { + "input": "./packages/remix", + "glob": "**.json", + "output": ".", + "ignore": ["**/tsconfig*.json", "project.json", ".eslintrc.json"] + }, + "LICENSE" + ] + } + }, + "add-extra-dependencies": { + "command": "node ./scripts/add-dependency-to-build.js remix @nrwl/remix" + } + }, + "tags": [] +} diff --git a/packages/remix/src/executors/build/build.impl.ts b/packages/remix/src/executors/build/build.impl.ts new file mode 100644 index 00000000000000..6788fe06a27962 --- /dev/null +++ b/packages/remix/src/executors/build/build.impl.ts @@ -0,0 +1,142 @@ +import { + detectPackageManager, + logger, + readJsonFile, + writeJsonFile, + type ExecutorContext, +} from '@nx/devkit'; +import { createLockFile, createPackageJson, getLockFileName } from '@nx/js'; +import { directoryExists } from '@nx/workspace/src/utilities/fileutils'; +import { fork } from 'child_process'; +import { copySync, mkdir, writeFileSync } from 'fs-extra'; +import { type PackageJson } from 'nx/src/utils/package-json'; +import { join } from 'path'; +import { type RemixBuildSchema } from './schema'; + +function buildRemixBuildArgs(options: RemixBuildSchema) { + const args = ['build']; + + if (options.sourcemap) { + args.push(`--sourcemap`); + } + + return args; +} + +async function runBuild( + options: RemixBuildSchema, + context: ExecutorContext +): Promise { + const projectRoot = context.projectGraph.nodes[context.projectName].data.root; + return new Promise((resolve, reject) => { + const remixBin = require.resolve('@remix-run/dev/dist/cli'); + const args = buildRemixBuildArgs(options); + const p = fork(remixBin, args, { + cwd: join(context.root, projectRoot), + stdio: 'inherit', + }); + p.on('exit', (code) => { + if (code === 0) resolve(); + else reject(); + }); + }); +} + +export default async function buildExecutor( + options: RemixBuildSchema, + context: ExecutorContext +) { + const projectRoot = context.projectGraph.nodes[context.projectName].data.root; + + try { + await runBuild(options, context); + } catch (error) { + logger.error( + `Error occurred while trying to build application. See above for more details.` + ); + return { success: false }; + } + + if (!directoryExists(options.outputPath)) { + mkdir(options.outputPath); + } + let packageJson: PackageJson; + if (options.generatePackageJson) { + packageJson = createPackageJson(context.projectName, context.projectGraph, { + target: context.targetName, + root: context.root, + isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build. + }); + + // Update `package.json` to reflect how users should run the build artifacts + packageJson.scripts ??= {}; + // Don't override existing custom script since project may have its own server. + if (!packageJson.scripts.start) { + packageJson.scripts['start'] = 'remix-serve ./build'; + } + + updatePackageJson(packageJson, context); + writeJsonFile(`${options.outputPath}/package.json`, packageJson); + } else { + packageJson = readJsonFile(join(projectRoot, 'package.json')); + } + + if (options.generateLockfile) { + const packageManager = detectPackageManager(context.root); + const lockFile = createLockFile( + packageJson, + context.projectGraph, + packageManager + ); + writeFileSync( + `${options.outputPath}/${getLockFileName(packageManager)}`, + lockFile, + { + encoding: 'utf-8', + } + ); + } + + // If output path is different from source path, then copy over the config and public files. + // This is the default behavior when running `nx build `. + if (options.outputPath.replace(/\/$/, '') !== projectRoot) { + copySync(join(projectRoot, 'public'), join(options.outputPath, 'public'), { + dereference: true, + }); + copySync(join(projectRoot, 'build'), join(options.outputPath, 'build'), { + dereference: true, + }); + } + + return { success: true }; +} + +function updatePackageJson(packageJson: PackageJson, context: ExecutorContext) { + if (!packageJson.scripts) { + packageJson.scripts = {}; + } + if (!packageJson.scripts.start) { + packageJson.scripts.start = 'remix-serve build'; + } + + packageJson.dependencies ??= {}; + + // These are always required for a production Remix app to run. + const requiredPackages = [ + 'react', + 'react-dom', + 'isbot', + 'typescript', + '@remix-run/css-bundle', + '@remix-run/node', + '@remix-run/react', + '@remix-run/serve', + '@remix-run/dev', + ]; + for (const pkg of requiredPackages) { + const externalNode = context.projectGraph.externalNodes[`npm:${pkg}`]; + if (externalNode) { + packageJson.dependencies[pkg] ??= externalNode.data.version; + } + } +} diff --git a/packages/remix/src/executors/build/schema.d.ts b/packages/remix/src/executors/build/schema.d.ts new file mode 100644 index 00000000000000..4002a7e166a7b9 --- /dev/null +++ b/packages/remix/src/executors/build/schema.d.ts @@ -0,0 +1,7 @@ +export interface RemixBuildSchema { + outputPath: string; + includeDevDependenciesInPackageJson?: boolean; + generatePackageJson?: boolean; + generateLockfile?: boolean; + sourcemap?: boolean; +} diff --git a/packages/remix/src/executors/build/schema.json b/packages/remix/src/executors/build/schema.json new file mode 100644 index 00000000000000..cd9e3c6bb71c8f --- /dev/null +++ b/packages/remix/src/executors/build/schema.json @@ -0,0 +1,38 @@ +{ + "version": 2, + "outputCapture": "pipe", + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "title": "Remix Build", + "description": "Build a Remix app.", + "type": "object", + "properties": { + "outputPath": { + "type": "string", + "description": "The output path of the generated files.", + "x-completion-type": "directory", + "x-priority": "important" + }, + "includeDevDependenciesInPackageJson": { + "type": "boolean", + "description": "Include `devDependencies` in the generated package.json file. By default only production `dependencies` are included.", + "default": false + }, + "generatePackageJson": { + "type": "boolean", + "description": "Generate package.json file in the output folder.", + "default": false + }, + "generateLockfile": { + "type": "boolean", + "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.", + "default": false + }, + "sourcemap": { + "type": "boolean", + "description": "Generate source maps for production.", + "default": false + } + }, + "required": ["outputPath"] +} diff --git a/packages/remix/src/executors/serve/schema.d.ts b/packages/remix/src/executors/serve/schema.d.ts new file mode 100644 index 00000000000000..61d67bd09a109b --- /dev/null +++ b/packages/remix/src/executors/serve/schema.d.ts @@ -0,0 +1,9 @@ +export interface RemixServeSchema { + port: number; + devServerPort?: number; + debug?: boolean; + command?: string; + manual?: boolean; + tlsKey?: string; + tlsCert?: string; +} diff --git a/packages/remix/src/executors/serve/schema.json b/packages/remix/src/executors/serve/schema.json new file mode 100644 index 00000000000000..1e8eff1573b917 --- /dev/null +++ b/packages/remix/src/executors/serve/schema.json @@ -0,0 +1,41 @@ +{ + "version": 2, + "outputCapture": "pipe", + "cli": "nx", + "title": "Remix Serve", + "description": "Serve a Remix app.", + "type": "object", + "properties": { + "port": { + "type": "number", + "description": "Set PORT environment variable that can be used to serve the Remix application.", + "default": 4200 + }, + "devServerPort": { + "type": "number", + "description": "Port to start the dev server on." + }, + "debug": { + "type": "boolean", + "description": "Attach a Node.js inspector.", + "default": false + }, + "command": { + "type": "string", + "description": "Command used to run your app server." + }, + "manual": { + "type": "boolean", + "description": "Enable manual mode", + "default": false + }, + "tlsKey": { + "type": "string", + "description": "Path to TLS key (key.pem)." + }, + "tlsCert": { + "type": "string", + "description": "Path to TLS certificate (cert.pem)." + } + } +} diff --git a/packages/remix/src/executors/serve/serve.impl.ts b/packages/remix/src/executors/serve/serve.impl.ts new file mode 100644 index 00000000000000..73429280da46e4 --- /dev/null +++ b/packages/remix/src/executors/serve/serve.impl.ts @@ -0,0 +1,95 @@ +import { workspaceRoot, type ExecutorContext } from '@nx/devkit'; +import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable'; +import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open'; +import { fork } from 'node:child_process'; +import { join } from 'node:path'; +import { type RemixServeSchema } from './schema'; + +function normalizeOptions(schema: RemixServeSchema) { + return { + ...schema, + port: schema.port ?? 4200, + debug: schema.debug ?? false, + manual: schema.manual ?? false, + } as RemixServeSchema; +} + +function buildRemixDevArgs(options: RemixServeSchema) { + const args = []; + + if (options.command) { + args.push(`--command=${options.command}`); + } + + if (options.devServerPort) { + args.push(`--port=${options.devServerPort}`); + } + + if (options.debug) { + args.push(`--debug`); + } + + if (options.manual) { + args.push(`--manual`); + } + + if (options.tlsKey) { + args.push(`--tls-key=${options.tlsKey}`); + } + + if (options.tlsCert) { + args.push(`--tls-cert=${options.tlsCert}`); + } + + return args; +} + +export default async function* serveExecutor( + schema: RemixServeSchema, + context: ExecutorContext +) { + const options = normalizeOptions(schema); + const projectRoot = context.workspace.projects[context.projectName].root; + + const remixBin = require.resolve('@remix-run/dev/dist/cli'); + const args = buildRemixDevArgs(options); + // Cast to any to overwrite NODE_ENV + (process.env as any).NODE_ENV = process.env.NODE_ENV + ? process.env.NODE_ENV + : 'development'; + process.env.PORT = `${options.port}`; + + yield* createAsyncIterable<{ success: boolean; baseUrl: string }>( + async ({ done, next, error }) => { + const server = fork(remixBin, ['dev', ...args], { + cwd: join(workspaceRoot, projectRoot), + stdio: 'inherit', + }); + + server.once('exit', (code) => { + if (code === 0) { + done(); + } else { + error(new Error(`Remix app exited with code ${code}`)); + } + }); + + const killServer = () => { + if (server.connected) { + server.kill('SIGTERM'); + } + }; + process.on('exit', () => killServer()); + process.on('SIGINT', () => killServer()); + process.on('SIGTERM', () => killServer()); + process.on('SIGHUP', () => killServer()); + + await waitForPortOpen(options.port); + + next({ + success: true, + baseUrl: `http://localhost:${options.port}`, + }); + } + ); +} diff --git a/packages/remix/src/generators/action/action.impl.spec.ts b/packages/remix/src/generators/action/action.impl.spec.ts new file mode 100644 index 00000000000000..0876f79a2cefc9 --- /dev/null +++ b/packages/remix/src/generators/action/action.impl.spec.ts @@ -0,0 +1,90 @@ +import { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from '../application/application.impl'; +import routeGenerator from '../route/route.impl'; +import actionGenerator from './action.impl'; + +describe('action', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write('.gitignore', `/node_modules/dist`); + + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + path: 'example', + project: 'demo', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + }); + + [ + { + path: 'apps/demo/app/routes/example.tsx', + }, + { + path: 'example', + }, + { + path: 'example.tsx', + }, + ].forEach((config) => { + describe(`Generating action using path ${config.path}`, () => { + beforeEach(async () => { + await actionGenerator(tree, { + path: config.path, + // path: 'apps/demo/app/routes/example.tsx', + project: 'demo', + }); + }); + it('should add imports', async () => { + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch(`import { json } from '@remix-run/node';`); + expect(content).toMatch( + `import type { ActionFunctionArgs } from '@remix-run/node';` + ); + expect(content).toMatch( + `import { useActionData } from '@remix-run/react';` + ); + }); + + it('should add action function', () => { + const actionFunction = `export const action = async ({ request }: ActionFunctionArgs)`; + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch(actionFunction); + }); + + it('should add useActionData to component', () => { + const useActionData = `const actionMessage = useActionData();`; + + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch(useActionData); + }); + }); + }); + + it('--nameAndDirectoryFormat=as-provided', async () => { + // ACT + await actionGenerator(tree, { + path: 'apps/demo/app/routes/example.tsx', + }); + // ASSERT + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + const useActionData = `const actionMessage = useActionData();`; + const actionFunction = `export const action = async ({ request }: ActionFunctionArgs)`; + expect(content).toMatch(`import { json } from '@remix-run/node';`); + expect(content).toMatch( + `import type { ActionFunctionArgs } from '@remix-run/node';` + ); + expect(content).toMatch( + `import { useActionData } from '@remix-run/react';` + ); + expect(content).toMatch(useActionData); + expect(content).toMatch(actionFunction); + }); +}); diff --git a/packages/remix/src/generators/action/action.impl.ts b/packages/remix/src/generators/action/action.impl.ts new file mode 100644 index 00000000000000..2f5fa8b660b76d --- /dev/null +++ b/packages/remix/src/generators/action/action.impl.ts @@ -0,0 +1,48 @@ +import { formatFiles, Tree } from '@nx/devkit'; +import { insertImport } from '../../utils/insert-import'; +import { insertStatementAfterImports } from '../../utils/insert-statement-after-imports'; +import { insertStatementInDefaultFunction } from '../../utils/insert-statement-in-default-function'; +import { resolveRemixRouteFile } from '../../utils/remix-route-utils'; +import { LoaderSchema } from './schema'; + +export default async function (tree: Tree, schema: LoaderSchema) { + const routeFilePath = + schema.nameAndDirectoryFormat === 'as-provided' + ? schema.path + : await resolveRemixRouteFile(tree, schema.path, schema.project); + + if (!tree.exists(routeFilePath)) { + throw new Error( + `Route path does not exist: ${routeFilePath}. Please generate a Remix route first.` + ); + } + + insertImport(tree, routeFilePath, 'ActionFunctionArgs', '@remix-run/node', { + typeOnly: true, + }); + insertImport(tree, routeFilePath, 'json', '@remix-run/node'); + insertImport(tree, routeFilePath, 'useActionData', '@remix-run/react'); + + insertStatementAfterImports( + tree, + routeFilePath, + ` + export const action = async ({ request }: ActionFunctionArgs) => { + let formData = await request.formData(); + + return json({message: formData.toString()}, { status: 200 }); + }; + + ` + ); + + const statement = `\nconst actionMessage = useActionData();`; + + try { + insertStatementInDefaultFunction(tree, routeFilePath, statement); + } catch (err) { + // eslint-disable-next-line no-empty + } finally { + await formatFiles(tree); + } +} diff --git a/packages/remix/src/generators/action/schema.d.ts b/packages/remix/src/generators/action/schema.d.ts new file mode 100644 index 00000000000000..9315409e926eeb --- /dev/null +++ b/packages/remix/src/generators/action/schema.d.ts @@ -0,0 +1,10 @@ +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +export interface LoaderSchema { + path: string; + nameAndDirectoryFormat?: NameAndDirectoryFormat; + /** + * @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18. + */ + project?: string; +} diff --git a/packages/remix/src/generators/action/schema.json b/packages/remix/src/generators/action/schema.json new file mode 100644 index 00000000000000..c5c947e57404ca --- /dev/null +++ b/packages/remix/src/generators/action/schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "action", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the action in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + } + }, + "required": ["path"] +} diff --git a/packages/remix/src/generators/application/__snapshots__/application.impl.spec.ts.snap b/packages/remix/src/generators/application/__snapshots__/application.impl.spec.ts.snap new file mode 100644 index 00000000000000..16f0dd1c2916eb --- /dev/null +++ b/packages/remix/src/generators/application/__snapshots__/application.impl.spec.ts.snap @@ -0,0 +1,1213 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --directory should create the application correctly 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --directory should create the application correctly 2`] = ` +"import type { MetaFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; + +export const meta: MetaFunction = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --directory should create the application correctly 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --directory should extract the layout directory from the directory options if it exists 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --directory should extract the layout directory from the directory options if it exists 2`] = ` +"import type { MetaFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; + +export const meta: MetaFunction = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --directory should extract the layout directory from the directory options if it exists 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --e2eTestRunner should generate an e2e application for the app 1`] = ` +"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: 'http://localhost:4200', + }, +}); +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --js should create the application correctly 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --js should create the application correctly 2`] = ` +"import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; +export const meta = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --js should create the application correctly 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --unitTestRunner should generate the correct files for testing using jest 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --unitTestRunner should generate the correct files for testing using jest 2`] = ` +"/* eslint-disable */ +export default { + setupFilesAfterEnv: ['/test-setup.ts'], + displayName: 'test', + preset: '../jest.preset.js', + transform: { + '^.+\\\\.[tj]sx?$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../coverage/test', +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --unitTestRunner should generate the correct files for testing using jest 3`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --unitTestRunner should generate the correct files for testing using vitest 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --unitTestRunner should generate the correct files for testing using vitest 2`] = ` +"/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../node_modules/.vite/test', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + setupFiles: ['./test-setup.ts'], + globals: true, + cache: { + dir: '../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['./app/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../coverage/test', + provider: 'v8', + }, + }, +}); +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided --unitTestRunner should generate the correct files for testing using vitest 3`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided should create the application correctly 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided should create the application correctly 2`] = ` +"import type { MetaFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; + +export const meta: MetaFunction = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=as-provided should create the application correctly 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --directory should create the application correctly 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --directory should create the application correctly 2`] = ` +"import type { MetaFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; + +export const meta: MetaFunction = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --directory should create the application correctly 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --directory should extract the layout directory from the directory options if it exists 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --directory should extract the layout directory from the directory options if it exists 2`] = ` +"import type { MetaFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; + +export const meta: MetaFunction = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --directory should extract the layout directory from the directory options if it exists 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --e2eTestRunner should generate an e2e application for the app 1`] = ` +"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: 'http://localhost:4200', + }, +}); +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --js should create the application correctly 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --js should create the application correctly 2`] = ` +"import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; +export const meta = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --js should create the application correctly 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --unitTestRunner should generate the correct files for testing using jest 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --unitTestRunner should generate the correct files for testing using jest 2`] = ` +"/* eslint-disable */ +export default { + setupFilesAfterEnv: ['/test-setup.ts'], + displayName: 'test', + preset: '../../jest.preset.js', + transform: { + '^.+\\\\.[tj]sx?$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/apps/test', +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --unitTestRunner should generate the correct files for testing using jest 3`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --unitTestRunner should generate the correct files for testing using vitest 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --unitTestRunner should generate the correct files for testing using vitest 2`] = ` +"/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/test', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + setupFiles: ['./test-setup.ts'], + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['./app/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/test', + provider: 'v8', + }, + }, +}); +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived --unitTestRunner should generate the correct files for testing using vitest 3`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived should create the application correctly 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived should create the application correctly 2`] = ` +"import type { MetaFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; + +export const meta: MetaFunction = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Integrated Repo --projectNameAndRootFormat=derived should create the application correctly 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Standalone Project Repo --e2eTestRunner should generate an e2e application for the app 1`] = ` +"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: 'http://localhost:4200', + }, +}); +" +`; + +exports[`Remix Application Standalone Project Repo --js should create the application correctly 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Standalone Project Repo --js should create the application correctly 2`] = ` +"import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; +export const meta = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Standalone Project Repo --js should create the application correctly 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; + +exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 2`] = ` +"/* eslint-disable */ +export default { + setupFilesAfterEnv: ['/test-setup.ts'], + displayName: 'test', + preset: './jest.preset.js', + transform: { + '^.+\\\\.[tj]sx?$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: './coverage/test', + testMatch: [ + '/src/**/__tests__/**/*.[jt]s?(x)', + '/src/**/*(*.)@(spec|test).[jt]s?(x)', + ], +}; +" +`; + +exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 3`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 2`] = ` +"/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: './node_modules/.vite/.', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + setupFiles: ['./test-setup.ts'], + globals: true, + cache: { + dir: './node_modules/.vitest', + }, + environment: 'jsdom', + include: ['./app/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: './coverage/.', + provider: 'v8', + }, + }, +}); +" +`; + +exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 3`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Application Standalone Project Repo should create the application correctly 1`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`Remix Application Standalone Project Repo should create the application correctly 2`] = ` +"import type { MetaFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; + +export const meta: MetaFunction = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`Remix Application Standalone Project Repo should create the application correctly 3`] = ` +"export default function Index() { + return ( + + ); +} +" +`; diff --git a/packages/remix/src/generators/application/application.impl.spec.ts b/packages/remix/src/generators/application/application.impl.spec.ts new file mode 100644 index 00000000000000..7461053b238e01 --- /dev/null +++ b/packages/remix/src/generators/application/application.impl.spec.ts @@ -0,0 +1,315 @@ +import type { Tree } from '@nx/devkit'; +import { joinPathFragments, readJson } from '@nx/devkit'; +import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from './application.impl'; + +describe('Remix Application', () => { + describe('Standalone Project Repo', () => { + it('should create the application correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + // ACT + await applicationGenerator(tree, { + name: 'test', + rootProject: true, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, '.'); + + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app/root.tsx', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app/routes/_index.tsx', 'utf-8')).toMatchSnapshot(); + }); + + describe(`--js`, () => { + it('should create the application correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + // ACT + await applicationGenerator(tree, { + name: 'test', + js: true, + rootProject: true, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, '.'); + + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app/root.js', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app/routes/_index.js', 'utf-8')).toMatchSnapshot(); + }); + }); + + describe('--unitTestRunner', () => { + it('should generate the correct files for testing using vitest', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + // ACT + await applicationGenerator(tree, { + name: 'test', + unitTestRunner: 'vitest', + rootProject: true, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, '.'); + + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot(); + expect(tree.read('vite.config.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.read('test-setup.ts', 'utf-8')).toMatchSnapshot(); + }); + + it('should generate the correct files for testing using jest', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + // ACT + await applicationGenerator(tree, { + name: 'test', + unitTestRunner: 'jest', + rootProject: true, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, '.'); + + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot(); + expect(tree.read('jest.config.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.read('test-setup.ts', 'utf-8')).toMatchSnapshot(); + }); + }); + + describe('--e2eTestRunner', () => { + it('should generate an e2e application for the app', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + // ACT + await applicationGenerator(tree, { + name: 'test', + e2eTestRunner: 'cypress', + rootProject: true, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, '.'); + + expect(tree.read('e2e/cypress.config.ts', 'utf-8')).toMatchSnapshot(); + }); + }); + }); + + describe.each([ + ['derived', 'apps/test', 'apps/test-e2e'], + ['as-provided', 'test', 'test-e2e'], + ])( + 'Integrated Repo --projectNameAndRootFormat=%s', + (projectNameAndRootFormat: ProjectNameAndRootFormat, appDir, e2eDir) => { + it('should create the application correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await applicationGenerator(tree, { + name: 'test', + projectNameAndRootFormat, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, appDir); + + expect( + tree.read(`${appDir}/remix.config.cjs`, 'utf-8') + ).toMatchSnapshot(); + expect(tree.read(`${appDir}/app/root.tsx`, 'utf-8')).toMatchSnapshot(); + expect( + tree.read(`${appDir}/app/routes/_index.tsx`, 'utf-8') + ).toMatchSnapshot(); + }); + + describe('--js', () => { + it('should create the application correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await applicationGenerator(tree, { + name: 'test', + js: true, + projectNameAndRootFormat, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, appDir); + + expect( + tree.read(`${appDir}/remix.config.cjs`, 'utf-8') + ).toMatchSnapshot(); + expect(tree.read(`${appDir}/app/root.js`, 'utf-8')).toMatchSnapshot(); + expect( + tree.read(`${appDir}/app/routes/_index.js`, 'utf-8') + ).toMatchSnapshot(); + }); + }); + describe('--directory', () => { + it('should create the application correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + const newAppDir = + projectNameAndRootFormat === 'as-provided' + ? 'demo' + : 'apps/demo/test'; + + // ACT + await applicationGenerator(tree, { + name: 'test', + directory: 'demo', + projectNameAndRootFormat, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, newAppDir); + + expect( + tree.read(`${newAppDir}/remix.config.cjs`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${newAppDir}/app/root.tsx`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${newAppDir}/app/routes/_index.tsx`, 'utf-8') + ).toMatchSnapshot(); + }); + + it('should extract the layout directory from the directory options if it exists', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + const newAppDir = + projectNameAndRootFormat === 'as-provided' + ? 'apps/demo' + : 'apps/demo/test'; + + // ACT + await applicationGenerator(tree, { + name: 'test', + directory: 'apps/demo', + projectNameAndRootFormat, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, newAppDir); + + expect( + tree.read(`${newAppDir}/remix.config.cjs`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${newAppDir}/app/root.tsx`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${newAppDir}/app/routes/_index.tsx`, 'utf-8') + ).toMatchSnapshot(); + }); + }); + + describe('--unitTestRunner', () => { + it('should generate the correct files for testing using vitest', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await applicationGenerator(tree, { + name: 'test', + unitTestRunner: 'vitest', + projectNameAndRootFormat, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, appDir); + + expect( + tree.read(`${appDir}/remix.config.cjs`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${appDir}/vite.config.ts`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${appDir}/test-setup.ts`, 'utf-8') + ).toMatchSnapshot(); + }); + + it('should generate the correct files for testing using jest', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await applicationGenerator(tree, { + name: 'test', + unitTestRunner: 'jest', + projectNameAndRootFormat, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, appDir); + + expect( + tree.read(`${appDir}/remix.config.cjs`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${appDir}/jest.config.ts`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${appDir}/test-setup.ts`, 'utf-8') + ).toMatchSnapshot(); + }); + }); + + describe('--e2eTestRunner', () => { + it('should generate an e2e application for the app', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await applicationGenerator(tree, { + name: 'test', + e2eTestRunner: 'cypress', + projectNameAndRootFormat, + }); + + // ASSERT + expectTargetsToBeCorrect(tree, appDir); + + expect( + tree.read(`${appDir}-e2e/cypress.config.ts`, 'utf-8') + ).toMatchSnapshot(); + }); + }); + } + ); +}); + +function expectTargetsToBeCorrect(tree: Tree, projectRoot: string) { + const { targets } = readJson( + tree, + joinPathFragments(projectRoot === '.' ? '/' : projectRoot, 'project.json') + ); + expect(targets.build).toBeTruthy(); + expect(targets.build.executor).toEqual('@nx/remix:build'); + expect(targets.build.options.outputPath).toEqual( + joinPathFragments('dist', projectRoot) + ); + expect(targets.serve).toBeTruthy(); + expect(targets.serve.executor).toEqual('@nx/remix:serve'); + expect(targets.serve.options.port).toEqual(4200); + expect(targets.start).toBeTruthy(); + expect(targets.start.command).toEqual('remix-serve build/index.js'); + expect(targets.start.options.cwd).toEqual(projectRoot); + expect(targets.typecheck).toBeTruthy(); + expect(targets.typecheck.command).toEqual('tsc'); + expect(targets.typecheck.options.cwd).toEqual(projectRoot); +} diff --git a/packages/remix/src/generators/application/application.impl.ts b/packages/remix/src/generators/application/application.impl.ts new file mode 100644 index 00000000000000..ee17937d8c23e4 --- /dev/null +++ b/packages/remix/src/generators/application/application.impl.ts @@ -0,0 +1,261 @@ +import { configurationGenerator } from '@nx/cypress'; +import { + addDependenciesToPackageJson, + addProjectConfiguration, + formatFiles, + generateFiles, + GeneratorCallback, + getPackageManagerCommand, + joinPathFragments, + offsetFromRoot, + readJson, + readProjectConfiguration, + runTasksInSerial, + toJS, + Tree, + updateJson, + updateProjectConfiguration, +} from '@nx/devkit'; +import { extractTsConfigBase } from '@nx/js/src/utils/typescript/create-ts-config'; +import { + eslintVersion, + getPackageVersion, + isbotVersion, + reactDomVersion, + reactVersion, + remixVersion, + typescriptVersion, + typesReactDomVersion, + typesReactVersion, +} from '../../utils/versions'; +import { + NormalizedSchema, + normalizeOptions, + updateUnitTestConfig, +} from './lib'; +import { NxRemixGeneratorSchema } from './schema'; +import { vitestGenerator } from '@nx/vite'; +import { configurationGenerator as jestConfigurationGenerator } from '@nx/jest'; + +export default async function (tree: Tree, _options: NxRemixGeneratorSchema) { + const options = await normalizeOptions(tree, _options); + const tasks: GeneratorCallback[] = []; + + addProjectConfiguration(tree, options.projectName, { + root: options.projectRoot, + sourceRoot: `${options.projectRoot}`, + projectType: 'application', + tags: options.parsedTags, + targets: { + build: { + executor: '@nx/remix:build', + outputs: ['{options.outputPath}'], + options: { + outputPath: joinPathFragments('dist', options.projectRoot), + }, + }, + serve: { + executor: `@nx/remix:serve`, + options: { + command: `${ + getPackageManagerCommand().exec + } remix-serve build/index.js`, + manual: true, + port: 4200, + }, + }, + start: { + dependsOn: ['build'], + command: `remix-serve build/index.js`, + options: { + cwd: options.projectRoot, + }, + }, + typecheck: { + command: `tsc`, + options: { + cwd: options.projectRoot, + }, + }, + }, + }); + + const installTask = addDependenciesToPackageJson( + tree, + { + '@remix-run/node': remixVersion, + '@remix-run/react': remixVersion, + '@remix-run/serve': remixVersion, + isbot: isbotVersion, + react: reactVersion, + 'react-dom': reactDomVersion, + }, + { + '@remix-run/dev': remixVersion, + '@remix-run/eslint-config': remixVersion, + '@types/react': typesReactVersion, + '@types/react-dom': typesReactDomVersion, + eslint: eslintVersion, + typescript: typescriptVersion, + } + ); + tasks.push(installTask); + + const vars = { + ...options, + tmpl: '', + offsetFromRoot: offsetFromRoot(options.projectRoot), + remixVersion, + isbotVersion, + reactVersion, + reactDomVersion, + typesReactVersion, + typesReactDomVersion, + eslintVersion, + typescriptVersion, + }; + + generateFiles( + tree, + joinPathFragments(__dirname, 'files/common'), + options.projectRoot, + vars + ); + + if (options.rootProject) { + const gitignore = tree.read('.gitignore', 'utf-8'); + tree.write( + '.gitignore', + `${gitignore}\n.cache\nbuild\npublic/build\n.env\n` + ); + } else { + generateFiles( + tree, + joinPathFragments(__dirname, 'files/integrated'), + options.projectRoot, + vars + ); + } + + if (options.unitTestRunner !== 'none') { + if (options.unitTestRunner === 'vitest') { + const vitestTask = await vitestGenerator(tree, { + uiFramework: 'react', + project: options.projectName, + coverageProvider: 'v8', + inSourceTests: false, + skipFormat: true, + testEnvironment: 'jsdom', + }); + tasks.push(vitestTask); + } else { + const jestTask = await jestConfigurationGenerator(tree, { + project: options.projectName, + setupFile: 'none', + supportTsx: true, + skipSerializers: false, + skipPackageJson: false, + skipFormat: true, + }); + const projectConfig = readProjectConfiguration(tree, options.projectName); + projectConfig.targets['test'].options.passWithNoTests = true; + updateProjectConfiguration(tree, options.projectName, projectConfig); + + tasks.push(jestTask); + } + + const pkgInstallTask = updateUnitTestConfig( + tree, + options.projectRoot, + options.unitTestRunner + ); + tasks.push(pkgInstallTask); + } else { + tree.delete( + joinPathFragments(options.projectRoot, `app/routes/_index.spec.tsx`) + ); + } + + if (options.js) { + toJS(tree); + } + + if (options.rootProject && tree.exists('tsconfig.base.json')) { + // If this is a standalone project, merge tsconfig.json and tsconfig.base.json. + const tsConfigBaseJson = readJson(tree, 'tsconfig.base.json'); + updateJson(tree, 'tsconfig.json', (json) => { + delete json.extends; + json.compilerOptions = { + ...tsConfigBaseJson.compilerOptions, + ...json.compilerOptions, + // Taken from remix default setup + // https://github.com/remix-run/remix/blob/68c8982/templates/remix/tsconfig.json#L15-L17 + paths: { + '~/*': ['./app/*'], + }, + }; + json.include = [ + ...(tsConfigBaseJson.include ?? []), + ...(json.include ?? []), + ]; + json.exclude = [ + ...(tsConfigBaseJson.exclude ?? []), + ...(json.exclude ?? []), + ]; + return json; + }); + tree.delete('tsconfig.base.json'); + } else { + // Otherwise, extract the tsconfig.base.json from tsconfig.json so we can share settings. + extractTsConfigBase(tree); + } + + if (options.e2eTestRunner === 'cypress') { + addFileServerTarget(tree, options, 'serve-static'); + addProjectConfiguration(tree, options.e2eProjectName, { + projectType: 'application', + root: options.e2eProjectRoot, + sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), + targets: {}, + tags: [], + implicitDependencies: [options.projectName], + }); + tasks.push( + await configurationGenerator(tree, { + project: options.e2eProjectName, + directory: 'src', + skipFormat: true, + devServerTarget: `${options.projectName}:serve:development`, + baseUrl: 'http://localhost:4200', + }) + ); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return runTasksInSerial(...tasks); +} + +function addFileServerTarget( + tree: Tree, + options: NormalizedSchema, + targetName: string +) { + addDependenciesToPackageJson( + tree, + {}, + { '@nx/web': getPackageVersion(tree, 'nx') } + ); + + const projectConfig = readProjectConfiguration(tree, options.projectName); + projectConfig.targets[targetName] = { + executor: '@nx/web:file-server', + options: { + buildTarget: `${options.projectName}:build`, + port: 4200, + }, + }; + updateProjectConfiguration(tree, options.projectName, projectConfig); +} diff --git a/packages/remix/src/generators/application/files/common/README.md__tmpl__ b/packages/remix/src/generators/application/files/common/README.md__tmpl__ new file mode 100644 index 00000000000000..3eaf4e37cb57ed --- /dev/null +++ b/packages/remix/src/generators/application/files/common/README.md__tmpl__ @@ -0,0 +1,54 @@ +# Welcome to Nx + Remix! + +- [Remix Docs](https://remix.run/docs) +- [Nx Docs](https://nx.dev) + +## Development + +From your terminal: + +```sh +npx nx dev <%= projectName %> +``` + +This starts your app in development mode, rebuilding assets on file changes. + +## Deployment + +First, build your app for production: + +```sh +npx nx build <%= projectName %> +``` + +Then run the app in production mode: + +```sh +npx nx start <%= projectName %> +``` + +Now you'll need to pick a host to deploy it to. + +### DIY + +If you're familiar with deploying node applications, the built-in Remix app server is production-ready. + +Make sure to deploy the output of `remix build` + +- `packages/<%= projectName %>/build/` +- `packages/<%= projectName %>/public/build/` + +### Using a Template + +When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server. + +```sh +cd .. +# create a new project, and pick a pre-configured host +npx create-remix@latest +cd my-new-remix-app +# remove the new project's app (not the old one!) +rm -rf app +# copy your app over +cp -R ../my-old-remix-app/app app +``` diff --git a/packages/remix/src/generators/application/files/common/app/root.tsx__tmpl__ b/packages/remix/src/generators/application/files/common/app/root.tsx__tmpl__ new file mode 100644 index 00000000000000..f7bdf668cab46d --- /dev/null +++ b/packages/remix/src/generators/application/files/common/app/root.tsx__tmpl__ @@ -0,0 +1,32 @@ +import type { MetaFunction } from "@remix-run/node"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +export const meta: MetaFunction = () => ([{ + charset: "utf-8", + title: "New Remix App", + viewport: "width=device-width,initial-scale=1", +}]); + +export default function App() { + return ( + + + + + + + + + + + + + ); +} diff --git a/packages/remix/src/generators/application/files/common/app/routes/_index.spec.tsx__tmpl__ b/packages/remix/src/generators/application/files/common/app/routes/_index.spec.tsx__tmpl__ new file mode 100644 index 00000000000000..fb59b9977c6809 --- /dev/null +++ b/packages/remix/src/generators/application/files/common/app/routes/_index.spec.tsx__tmpl__ @@ -0,0 +1,16 @@ +import { createRemixStub } from '@remix-run/testing'; +import { render, screen, waitFor } from '@testing-library/react'; +import Index from './_index'; + +test('renders loader data', async () => { + const RemixStub = createRemixStub([ + { + path: '/', + Component: Index, + }, + ]); + + render(); + + await waitFor(() => screen.findByText('Welcome to Remix')); +}); diff --git a/packages/remix/src/generators/application/files/common/app/routes/_index.tsx__tmpl__ b/packages/remix/src/generators/application/files/common/app/routes/_index.tsx__tmpl__ new file mode 100644 index 00000000000000..43e520bf3070f5 --- /dev/null +++ b/packages/remix/src/generators/application/files/common/app/routes/_index.tsx__tmpl__ @@ -0,0 +1,32 @@ +export default function Index() { + return ( + + ); +} \ No newline at end of file diff --git a/packages/remix/src/generators/application/files/common/public/favicon.ico b/packages/remix/src/generators/application/files/common/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8830cf6821b354114848e6354889b8ecf6d2bc61 GIT binary patch literal 16958 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 EKbET!0{{R3 literal 0 HcmV?d00001 diff --git a/packages/remix/src/generators/application/files/common/remix.config.cjs__tmpl__ b/packages/remix/src/generators/application/files/common/remix.config.cjs__tmpl__ new file mode 100644 index 00000000000000..c2beffa337c1a2 --- /dev/null +++ b/packages/remix/src/generators/application/files/common/remix.config.cjs__tmpl__ @@ -0,0 +1,11 @@ +/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ["**/.*"], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require("@nx/remix").createWatchPaths(__dirname), +}; diff --git a/packages/remix/src/generators/application/files/common/remix.env.d.ts__tmpl__ b/packages/remix/src/generators/application/files/common/remix.env.d.ts__tmpl__ new file mode 100644 index 00000000000000..dcf8c45e1d4cf6 --- /dev/null +++ b/packages/remix/src/generators/application/files/common/remix.env.d.ts__tmpl__ @@ -0,0 +1,2 @@ +/// +/// diff --git a/packages/remix/src/generators/application/files/common/tsconfig.json__tmpl__ b/packages/remix/src/generators/application/files/common/tsconfig.json__tmpl__ new file mode 100644 index 00000000000000..058aafdd278fd3 --- /dev/null +++ b/packages/remix/src/generators/application/files/common/tsconfig.json__tmpl__ @@ -0,0 +1,18 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2019", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + // Remix takes care of building everything in `remix build`. + "noEmit": true + } +} diff --git a/packages/remix/src/generators/application/files/integrated/.gitignore__tmpl__ b/packages/remix/src/generators/application/files/integrated/.gitignore__tmpl__ new file mode 100644 index 00000000000000..9ca4842f9caf46 --- /dev/null +++ b/packages/remix/src/generators/application/files/integrated/.gitignore__tmpl__ @@ -0,0 +1,4 @@ +.cache +build +public/build +.env diff --git a/packages/remix/src/generators/application/files/integrated/package.json__tmpl__ b/packages/remix/src/generators/application/files/integrated/package.json__tmpl__ new file mode 100644 index 00000000000000..fb01011bd71794 --- /dev/null +++ b/packages/remix/src/generators/application/files/integrated/package.json__tmpl__ @@ -0,0 +1,28 @@ +{ + "private": true, + "name": "<%= projectName %>", + "description": "", + "license": "", + "scripts": {}, + "type": "module", + "dependencies": { + "@remix-run/node": "<%= remixVersion %>", + "@remix-run/react": "<%= remixVersion %>", + "@remix-run/serve": "<%= remixVersion %>", + "isbot": "<%= isbotVersion %>", + "react": "<%= reactVersion %>", + "react-dom": "<%= reactDomVersion %>" + }, + "devDependencies": { + "@remix-run/dev": "<%= remixVersion %>", + "@remix-run/eslint-config": "<%= remixVersion %>", + "@types/react": "<%= typesReactVersion %>", + "@types/react-dom": "<%= typesReactDomVersion %>", + "eslint": "<%= eslintVersion %>", + "typescript": "<%= typescriptVersion %>" + }, + "engines": { + "node": ">=14" + }, + "sideEffects": false +} diff --git a/packages/remix/src/generators/application/lib/index.ts b/packages/remix/src/generators/application/lib/index.ts new file mode 100644 index 00000000000000..df3573ed82c11a --- /dev/null +++ b/packages/remix/src/generators/application/lib/index.ts @@ -0,0 +1,2 @@ +export * from './normalize-options'; +export * from './update-unit-test-config'; diff --git a/packages/remix/src/generators/application/lib/normalize-options.ts b/packages/remix/src/generators/application/lib/normalize-options.ts new file mode 100644 index 00000000000000..82d9f0e735cc0f --- /dev/null +++ b/packages/remix/src/generators/application/lib/normalize-options.ts @@ -0,0 +1,43 @@ +import { type Tree } from '@nx/devkit'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { type NxRemixGeneratorSchema } from '../schema'; + +export interface NormalizedSchema extends NxRemixGeneratorSchema { + projectName: string; + projectRoot: string; + e2eProjectName: string; + e2eProjectRoot: string; + parsedTags: string[]; +} + +export async function normalizeOptions( + tree: Tree, + options: NxRemixGeneratorSchema +): Promise { + const { projectName, projectRoot, projectNameAndRootFormat } = + await determineProjectNameAndRootOptions(tree, { + name: options.name, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + rootProject: options.rootProject, + callingGenerator: '@nx/remix:application', + }); + options.rootProject = projectRoot === '.'; + options.projectNameAndRootFormat = projectNameAndRootFormat; + const e2eProjectName = options.rootProject ? 'e2e' : `${projectName}-e2e`; + const e2eProjectRoot = options.rootProject ? 'e2e' : `${projectRoot}-e2e`; + + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + return { + ...options, + projectName, + projectRoot, + e2eProjectName, + e2eProjectRoot, + parsedTags, + }; +} diff --git a/packages/remix/src/generators/application/lib/update-unit-test-config.ts b/packages/remix/src/generators/application/lib/update-unit-test-config.ts new file mode 100644 index 00000000000000..bedd544bbbb185 --- /dev/null +++ b/packages/remix/src/generators/application/lib/update-unit-test-config.ts @@ -0,0 +1,57 @@ +import { + addDependenciesToPackageJson, + joinPathFragments, + stripIndents, + type Tree, +} from '@nx/devkit'; +import { + updateJestTestSetup, + updateViteTestIncludes, + updateViteTestSetup, +} from '../../../utils/testing-config-utils'; +import { + getRemixVersion, + testingLibraryJestDomVersion, + testingLibraryReactVersion, + testingLibraryUserEventsVersion, +} from '../../../utils/versions'; + +export function updateUnitTestConfig( + tree: Tree, + pathToRoot: string, + unitTestRunner: 'vitest' | 'jest' +) { + const pathToTestSetup = joinPathFragments(pathToRoot, `test-setup.ts`); + tree.write( + pathToTestSetup, + stripIndents` + import { installGlobals } from '@remix-run/node'; + import '@testing-library/jest-dom/matchers'; + installGlobals();` + ); + + if (unitTestRunner === 'vitest') { + const pathToViteConfig = joinPathFragments(pathToRoot, 'vite.config.ts'); + updateViteTestIncludes( + tree, + pathToViteConfig, + './app/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}' + ); + updateViteTestSetup(tree, pathToViteConfig, './test-setup.ts'); + } else if (unitTestRunner === 'jest') { + const pathToJestConfig = joinPathFragments(pathToRoot, 'jest.config.ts'); + updateJestTestSetup(tree, pathToJestConfig, `/test-setup.ts`); + } + + return addDependenciesToPackageJson( + tree, + {}, + { + '@testing-library/jest-dom': testingLibraryJestDomVersion, + '@testing-library/react': testingLibraryReactVersion, + '@testing-library/user-event': testingLibraryUserEventsVersion, + '@remix-run/node': getRemixVersion(tree), + '@remix-run/testing': getRemixVersion(tree), + } + ); +} diff --git a/packages/remix/src/generators/application/schema.d.ts b/packages/remix/src/generators/application/schema.d.ts new file mode 100644 index 00000000000000..6a5024d8b54a71 --- /dev/null +++ b/packages/remix/src/generators/application/schema.d.ts @@ -0,0 +1,13 @@ +import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; + +export interface NxRemixGeneratorSchema { + name: string; + tags?: string; + js?: boolean; + directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; + unitTestRunner?: 'vitest' | 'jest' | 'none'; + e2eTestRunner?: 'cypress' | 'none'; + skipFormat?: boolean; + rootProject?: boolean; +} diff --git a/packages/remix/src/generators/application/schema.json b/packages/remix/src/generators/application/schema.json new file mode 100644 index 00000000000000..dd7d8509a842c0 --- /dev/null +++ b/packages/remix/src/generators/application/schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixApplication", + "title": "Create an Application", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What is the name of the application?" + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files.", + "default": false + }, + "directory": { + "type": "string", + "description": "A directory where the app is placed.", + "alias": "dir", + "x-priority": "important" + }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "unitTestRunner": { + "type": "string", + "enum": ["vitest", "jest", "none"], + "default": "vitest", + "description": "Test runner to use for unit tests.", + "x-prompt": "What unit test runner should be used?" + }, + "e2eTestRunner": { + "type": "string", + "enum": ["cypress", "none"], + "default": "cypress", + "description": "Test runner to use for e2e tests" + }, + "tags": { + "type": "string", + "description": "Add tags to the project (used for linting)", + "alias": "t" + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files", + "default": false + }, + "rootProject": { + "type": "boolean", + "x-priority": "internal", + "default": false + } + } +} diff --git a/packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.spec.ts b/packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.spec.ts new file mode 100644 index 00000000000000..f402f81f9649e8 --- /dev/null +++ b/packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.spec.ts @@ -0,0 +1,51 @@ +import { joinPathFragments, readProjectConfiguration } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import libraryGenerator from '../library/library.impl'; +import cypressComponentConfigurationGenerator from './cypress-component-configuration.impl'; + +describe('CypressComponentConfiguration', () => { + it('should create the cypress configuration correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + await libraryGenerator(tree, { + name: 'cypress-test', + unitTestRunner: 'vitest', + style: 'css', + }); + + // ACT + await cypressComponentConfigurationGenerator(tree, { + project: 'cypress-test', + generateTests: true, + }); + + // ASSERT + const project = readProjectConfiguration(tree, 'cypress-test'); + expect( + tree.read(joinPathFragments(project.root, 'cypress.config.ts'), 'utf-8') + ).toMatchInlineSnapshot(` + "import { defineConfig } from 'cypress'; + import { nxComponentTestingPreset } from '@nx/remix/plugins/component-testing'; + + export default defineConfig({ + component: nxComponentTestingPreset(__filename), + }); + " + `); + expect(project.targets['component-test']).toMatchInlineSnapshot(` + { + "executor": "@nx/cypress:cypress", + "options": { + "cypressConfig": "cypress-test/cypress.config.ts", + "devServerTarget": "", + "skipServe": true, + "testingType": "component", + }, + } + `); + expect( + tree.exists(joinPathFragments(project.root, 'cypress')) + ).toBeTruthy(); + }); +}); diff --git a/packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.ts b/packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.ts new file mode 100644 index 00000000000000..399e4109cd7317 --- /dev/null +++ b/packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.ts @@ -0,0 +1,30 @@ +import { + formatFiles, + generateFiles, + readProjectConfiguration, + type Tree, +} from '@nx/devkit'; +import { join } from 'path'; +import { type CypressComponentConfigurationSchema } from './schema'; +import { cypressComponentConfigGenerator } from '@nx/react'; + +export default async function cypressComponentConfigurationGenerator( + tree: Tree, + options: CypressComponentConfigurationSchema +) { + await cypressComponentConfigGenerator(tree, { + project: options.project, + generateTests: options.generateTests, + skipFormat: true, + bundler: 'vite', + buildTarget: '', + }); + + const project = readProjectConfiguration(tree, options.project); + + generateFiles(tree, join(__dirname, './files'), project.root, { tmpl: '' }); + + if (!options.skipFormat) { + await formatFiles(tree); + } +} diff --git a/packages/remix/src/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ b/packages/remix/src/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ new file mode 100644 index 00000000000000..6b04b7c25247d6 --- /dev/null +++ b/packages/remix/src/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ @@ -0,0 +1,6 @@ +import {defineConfig} from 'cypress'; +import {nxComponentTestingPreset} from '@nx/remix/plugins/component-testing'; + +export default defineConfig({ + component: nxComponentTestingPreset(__filename), +}); diff --git a/packages/remix/src/generators/cypress-component-configuration/schema.d.ts b/packages/remix/src/generators/cypress-component-configuration/schema.d.ts new file mode 100644 index 00000000000000..be3f161cc0757c --- /dev/null +++ b/packages/remix/src/generators/cypress-component-configuration/schema.d.ts @@ -0,0 +1,5 @@ +export interface CypressComponentConfigurationSchema { + project: string; + generateTests?: boolean; + skipFormat?: boolean; +} diff --git a/packages/remix/src/generators/cypress-component-configuration/schema.json b/packages/remix/src/generators/cypress-component-configuration/schema.json new file mode 100644 index 00000000000000..27f17c56e150bb --- /dev/null +++ b/packages/remix/src/generators/cypress-component-configuration/schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/schema", + "cli": "nx", + "$id": "NxRemixCypressComponentTestConfiguration", + "title": "Add Cypress component testing", + "description": "Add a Cypress component testing configuration to an existing project.", + "type": "object", + "examples": [ + { + "command": "nx g @nx/remix:cypress-component-configuration --project=my-remix-project", + "description": "Add component testing to your Remix project" + }, + { + "command": "nx g @nx/remix:cypress-component-configuration --project=my-remix-project --generate-tests", + "description": "Add component testing to your Remix project and generate component tests for your existing components" + } + ], + "properties": { + "project": { + "type": "string", + "description": "The name of the project to add cypress component testing configuration to", + "x-dropdown": "projects", + "x-prompt": "What project should we add Cypress component testing to?", + "x-priority": "important" + }, + "generateTests": { + "type": "boolean", + "description": "Generate default component tests for existing components in the project", + "x-prompt": "Automatically generate tests for components declared in this project?", + "default": false, + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files", + "default": false, + "x-priority": "internal" + } + }, + "required": ["project"] +} diff --git a/packages/remix/src/generators/cypress/cypress.impl.spec.ts b/packages/remix/src/generators/cypress/cypress.impl.spec.ts new file mode 100644 index 00000000000000..5637dd3f8b2880 --- /dev/null +++ b/packages/remix/src/generators/cypress/cypress.impl.spec.ts @@ -0,0 +1,35 @@ +import { readProjectConfiguration, Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import generator from './cypress.impl'; +import applicationGenerator from '../application/application.impl'; + +describe('Cypress generator', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should generate cypress project', async () => { + await applicationGenerator(tree, { name: 'demo', e2eTestRunner: 'none' }); + await generator(tree, { project: 'demo', name: 'demo-e2e' }); + + const config = readProjectConfiguration(tree, 'demo-e2e'); + expect(config.targets).toEqual({ + e2e: { + executor: '@nx/cypress:cypress', + options: { + cypressConfig: 'demo-e2e/cypress.config.ts', + testingType: 'e2e', + devServerTarget: 'demo:serve:development', + }, + configurations: { + ci: { + devServerTarget: 'demo:serve-static', + }, + }, + }, + lint: { executor: '@nx/eslint:lint', outputs: ['{options.outputFile}'] }, + }); + }); +}); diff --git a/packages/remix/src/generators/cypress/cypress.impl.ts b/packages/remix/src/generators/cypress/cypress.impl.ts new file mode 100644 index 00000000000000..dc7c92e2eadd94 --- /dev/null +++ b/packages/remix/src/generators/cypress/cypress.impl.ts @@ -0,0 +1,113 @@ +import { + addDependenciesToPackageJson, + addProjectConfiguration, + GeneratorCallback, + joinPathFragments, + readProjectConfiguration, + runTasksInSerial, + Tree, + updateProjectConfiguration, +} from '@nx/devkit'; +import { configurationGenerator } from '@nx/cypress'; +import { CypressGeneratorSchema } from './schema'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { nxVersion } from '../../utils/versions'; + +export default async function ( + tree: Tree, + options: CypressGeneratorSchema +): Promise { + const { projectName: e2eProjectName, projectRoot: e2eProjectRoot } = + await determineProjectNameAndRootOptions(tree, { + name: options.name, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/remix:cypress', + }); + const rootProject = e2eProjectRoot === '.'; + let projectConfig = readProjectConfiguration(tree, options.project); + options.baseUrl ??= `http://localhost:${projectConfig.targets['serve'].options.port}`; + + addFileServerTarget(tree, options, 'serve-static'); + addProjectConfiguration(tree, e2eProjectName, { + projectType: 'application', + root: e2eProjectRoot, + sourceRoot: joinPathFragments(e2eProjectRoot, 'src'), + targets: {}, + tags: [], + implicitDependencies: [options.name], + }); + const installTask = await configurationGenerator(tree, { + project: e2eProjectName, + directory: 'src', + linter: options.linter, + skipPackageJson: false, + skipFormat: true, + devServerTarget: `${options.project}:serve:development`, + baseUrl: options.baseUrl, + rootProject, + }); + + projectConfig = readProjectConfiguration(tree, e2eProjectName); + + tree.delete( + joinPathFragments(projectConfig.sourceRoot, 'support', 'app.po.ts') + ); + tree.write( + joinPathFragments(projectConfig.sourceRoot, 'e2e', 'app.cy.ts'), + `describe('webapp', () => { + beforeEach(() => cy.visit('/')); + + it('should display welcome message', () => { + cy.get('h1').contains('Welcome to Remix'); + }); +});` + ); + + const supportFilePath = joinPathFragments( + projectConfig.sourceRoot, + 'support', + 'e2e.ts' + ); + const supportContent = tree.read(supportFilePath, 'utf-8'); + + tree.write( + supportFilePath, + `${supportContent} + +// from https://github.com/remix-run/indie-stack +Cypress.on("uncaught:exception", (err) => { + // Cypress and React Hydrating the document don't get along + // for some unknown reason. Hopefully we figure out why eventually + // so we can remove this. + if ( + /hydrat/i.test(err.message) || + /Minified React error #418/.test(err.message) || + /Minified React error #423/.test(err.message) + ) { + return false; + } +});` + ); + + return runTasksInSerial(installTask); +} + +function addFileServerTarget( + tree: Tree, + options: CypressGeneratorSchema, + targetName: string +) { + addDependenciesToPackageJson(tree, {}, { '@nx/web': nxVersion }); + + const projectConfig = readProjectConfiguration(tree, options.project); + projectConfig.targets[targetName] = { + executor: '@nx/web:file-server', + options: { + buildTarget: `${options.project}:build`, + port: projectConfig.targets['serve'].options.port, + }, + }; + updateProjectConfiguration(tree, options.project, projectConfig); +} diff --git a/packages/remix/src/generators/cypress/schema.d.ts b/packages/remix/src/generators/cypress/schema.d.ts new file mode 100644 index 00000000000000..ddd64b3936e0c1 --- /dev/null +++ b/packages/remix/src/generators/cypress/schema.d.ts @@ -0,0 +1,14 @@ +import { type ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { Linter } from '@nx/eslint'; + +export interface CypressGeneratorSchema { + project: string; + name: string; + baseUrl?: string; + directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; + linter?: Linter; + js?: boolean; + skipFormat?: boolean; + setParserOptionsProject?: boolean; +} diff --git a/packages/remix/src/generators/cypress/schema.json b/packages/remix/src/generators/cypress/schema.json new file mode 100644 index 00000000000000..df5b19f71bf171 --- /dev/null +++ b/packages/remix/src/generators/cypress/schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixCypress", + "title": "", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the frontend project to test.", + "$default": { + "$source": "projectName" + } + }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "baseUrl": { + "type": "string", + "description": "URL to access the application on", + "default": "http://localhost:3000" + }, + "name": { + "type": "string", + "description": "Name of the E2E Project", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the e2e project?" + }, + "directory": { + "type": "string", + "description": "A directory where the project is placed" + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "none"], + "default": "eslint" + }, + "js": { + "description": "Generate JavaScript files rather than TypeScript files", + "type": "boolean", + "default": false + }, + "skipFormat": { + "description": "Skip formatting files", + "type": "boolean", + "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false + } + }, + "required": ["name"] +} diff --git a/packages/remix/src/generators/error-boundary/__snapshots__/error-boundary.impl.spec.ts.snap b/packages/remix/src/generators/error-boundary/__snapshots__/error-boundary.impl.spec.ts.snap new file mode 100644 index 00000000000000..aa89f2fc78eac0 --- /dev/null +++ b/packages/remix/src/generators/error-boundary/__snapshots__/error-boundary.impl.spec.ts.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorBoundary --nameAndDirectoryFormat=as-provided --apiVersion=2 should correctly add the ErrorBoundary to the route file 1`] = ` +"import { useRouteError, isRouteErrorResponse } from '@remix-run/react'; +export function ErrorBoundary() { + const error = useRouteError(); + + // when true, this is what used to go to 'CatchBoundary' + if (isRouteErrorResponse(error)) { + return ( +
+

Oops

+

Status: {error.status}

+

{error.data.message}

+
+ ); + } else if (error instanceof Error) { + return ( +
+

Error

+

{error.message}

+

The stack trace is:

+
{error.stack}
+
+ ); + } else { + return

Unknown Error

; + } +} +" +`; + +exports[`ErrorBoundary --nameAndDirectoryFormat=as-provided --apiVersion=2 should correctly add the ErrorBoundary to the route file 2`] = ` +"import { useRouteError, isRouteErrorResponse } from '@remix-run/react'; +export function ErrorBoundary() { + const error = useRouteError(); + + // when true, this is what used to go to 'CatchBoundary' + if (isRouteErrorResponse(error)) { + return ( +
+

Oops

+

Status: {error.status}

+

{error.data.message}

+
+ ); + } else if (error instanceof Error) { + return ( +
+

Error

+

{error.message}

+

The stack trace is:

+
{error.stack}
+
+ ); + } else { + return

Unknown Error

; + } +} +" +`; diff --git a/packages/remix/src/generators/error-boundary/error-boundary.impl.spec.ts b/packages/remix/src/generators/error-boundary/error-boundary.impl.spec.ts new file mode 100644 index 00000000000000..6e6198ea3fc639 --- /dev/null +++ b/packages/remix/src/generators/error-boundary/error-boundary.impl.spec.ts @@ -0,0 +1,67 @@ +import { addProjectConfiguration } from '@nx/devkit'; +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import errorBoundaryGenerator from './error-boundary.impl'; + +describe('ErrorBoundary', () => { + describe.each([ + ['derived', 'app/routes/test.tsx', 'demo'], + ['as-provided', 'app/routes/test.tsx', ''], + ])( + `--nameAndDirectoryFormat=as-provided`, + ( + nameAndDirectoryFormat: NameAndDirectoryFormat, + routeFilePath: string, + project: string + ) => { + describe('--apiVersion=2', () => { + it('should correctly add the ErrorBoundary to the route file', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'demo', { + name: 'demo', + root: '.', + sourceRoot: '.', + projectType: 'application', + }); + const routeFilePath = `app/routes/test.tsx`; + tree.write(routeFilePath, ``); + tree.write('remix.config.cjs', `module.exports = {}`); + + // ACT + await errorBoundaryGenerator(tree, { + project, + nameAndDirectoryFormat, + path: routeFilePath, + }); + + // ASSERT + expect(tree.read(routeFilePath, 'utf-8')).toMatchSnapshot(); + }); + + it('should error when the route file cannot be found', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'demo', { + name: 'demo', + root: '.', + sourceRoot: '.', + projectType: 'application', + }); + const routeFilePath = `app/routes/test.tsx`; + tree.write(routeFilePath, ``); + tree.write('remix.config.cjs', `module.exports = {}`); + + // ACT & ASSERT + await expect( + errorBoundaryGenerator(tree, { + project, + nameAndDirectoryFormat, + path: `my-route.tsx`, + }) + ).rejects.toThrow(); + }); + }); + } + ); +}); diff --git a/packages/remix/src/generators/error-boundary/error-boundary.impl.ts b/packages/remix/src/generators/error-boundary/error-boundary.impl.ts new file mode 100644 index 00000000000000..fa1a7e22f8bf1f --- /dev/null +++ b/packages/remix/src/generators/error-boundary/error-boundary.impl.ts @@ -0,0 +1,16 @@ +import { formatFiles, type Tree } from '@nx/devkit'; +import { addV2ErrorBoundary, normalizeOptions } from './lib'; +import type { ErrorBoundarySchema } from './schema'; + +export default async function errorBoundaryGenerator( + tree: Tree, + schema: ErrorBoundarySchema +) { + const options = await normalizeOptions(tree, schema); + + addV2ErrorBoundary(tree, options); + + if (!options.skipFormat) { + await formatFiles(tree); + } +} diff --git a/packages/remix/src/generators/error-boundary/lib/add-v2-error-boundary.ts b/packages/remix/src/generators/error-boundary/lib/add-v2-error-boundary.ts new file mode 100644 index 00000000000000..381384f8576fe9 --- /dev/null +++ b/packages/remix/src/generators/error-boundary/lib/add-v2-error-boundary.ts @@ -0,0 +1,41 @@ +import { stripIndents, type Tree } from '@nx/devkit'; +import { insertImport } from '../../../utils/insert-import'; +import { insertStatementAfterImports } from '../../../utils/insert-statement-after-imports'; +import type { ErrorBoundarySchema } from '../schema'; + +export function addV2ErrorBoundary(tree: Tree, options: ErrorBoundarySchema) { + insertImport(tree, options.path, `useRouteError`, '@remix-run/react'); + insertImport(tree, options.path, `isRouteErrorResponse`, '@remix-run/react'); + + insertStatementAfterImports( + tree, + options.path, + stripIndents` + export function ErrorBoundary() { + const error = useRouteError(); + + // when true, this is what used to go to 'CatchBoundary' + if (isRouteErrorResponse(error)) { + return ( +
+

Oops

+

Status: {error.status}

+

{error.data.message}

+
+ ); + } else if (error instanceof Error) { + return ( +
+

Error

+

{error.message}

+

The stack trace is:

+
{error.stack}
+
+ ); + } else { + return

Unknown Error

; + } + } + ` + ); +} diff --git a/packages/remix/src/generators/error-boundary/lib/index.ts b/packages/remix/src/generators/error-boundary/lib/index.ts new file mode 100644 index 00000000000000..0cce967d13b304 --- /dev/null +++ b/packages/remix/src/generators/error-boundary/lib/index.ts @@ -0,0 +1,2 @@ +export * from './add-v2-error-boundary'; +export * from './normalize-options'; diff --git a/packages/remix/src/generators/error-boundary/lib/normalize-options.ts b/packages/remix/src/generators/error-boundary/lib/normalize-options.ts new file mode 100644 index 00000000000000..d1505fc3b04da5 --- /dev/null +++ b/packages/remix/src/generators/error-boundary/lib/normalize-options.ts @@ -0,0 +1,24 @@ +import { type Tree } from '@nx/devkit'; +import { resolveRemixRouteFile } from '../../../utils/remix-route-utils'; +import type { ErrorBoundarySchema } from '../schema'; + +export async function normalizeOptions( + tree: Tree, + schema: ErrorBoundarySchema +): Promise { + const pathToRouteFile = + schema.nameAndDirectoryFormat === 'as-provided' + ? schema.path + : await resolveRemixRouteFile(tree, schema.path, schema.project); + + if (!tree.exists(pathToRouteFile)) { + throw new Error( + `Route file specified does not exist "${pathToRouteFile}". Please ensure you pass a correct path to the file.` + ); + } + + return { + ...schema, + path: pathToRouteFile, + }; +} diff --git a/packages/remix/src/generators/error-boundary/schema.d.ts b/packages/remix/src/generators/error-boundary/schema.d.ts new file mode 100644 index 00000000000000..9c4893aa6208e1 --- /dev/null +++ b/packages/remix/src/generators/error-boundary/schema.d.ts @@ -0,0 +1,11 @@ +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +export interface ErrorBoundarySchema { + path: string; + skipFormat?: false; + nameAndDirectoryFormat?: NameAndDirectoryFormat; + /** + * @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18. + */ + project?: string; +} diff --git a/packages/remix/src/generators/error-boundary/schema.json b/packages/remix/src/generators/error-boundary/schema.json new file mode 100644 index 00000000000000..9d9a272d1c7bfb --- /dev/null +++ b/packages/remix/src/generators/error-boundary/schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixErrorBoundary", + "title": "Create an ErrorBoundary for a Route", + "type": "object", + "examples": [ + { + "command": "g error-boundary --routePath=apps/demo/app/routes/my-route.tsx", + "description": "Generate an ErrorBoundary for my-route.tsx" + } + ], + "properties": { + "path": { + "type": "string", + "description": "The path to route file relative to the project root." + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the error boundary in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What project contains the route file that this ErrorBoundary is for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files after generation.", + "default": false, + "x-priority": "internal" + } + }, + "required": ["path"] +} diff --git a/packages/remix/src/generators/library/__snapshots__/library.impl.spec.ts.snap b/packages/remix/src/generators/library/__snapshots__/library.impl.spec.ts.snap new file mode 100644 index 00000000000000..cd70040d87f7c0 --- /dev/null +++ b/packages/remix/src/generators/library/__snapshots__/library.impl.spec.ts.snap @@ -0,0 +1,157 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Remix Library Generator -projectNameAndRootFormat=as-provided --unitTestRunner should create the correct config files for testing with jest 1`] = ` +"/* eslint-disable */ +export default { + setupFilesAfterEnv: ['./src/test-setup.ts'], + displayName: 'test', + preset: '../jest.preset.js', + transform: { + '^(?!.*\\\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../coverage/test', +}; +" +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=as-provided --unitTestRunner should create the correct config files for testing with jest 2`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=as-provided --unitTestRunner should create the correct config files for testing with vitest 1`] = ` +"import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../node_modules/.vite/test', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + setupFiles: ['./src/test-setup.ts'], + globals: true, + cache: { dir: '../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { reportsDirectory: '../coverage/test', provider: 'v8' }, + }, +}); +" +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=as-provided --unitTestRunner should create the correct config files for testing with vitest 2`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=as-provided should generate a library correctly 1`] = ` +[ + "test.module.css", + "test.spec.tsx", + "test.tsx", +] +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=as-provided should generate a library correctly 2`] = ` +{ + "@proj/test": [ + "test/src/index.ts", + ], + "@proj/test/server": [ + "test/src/server.ts", + ], +} +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=derived --unitTestRunner should create the correct config files for testing with jest 1`] = ` +"/* eslint-disable */ +export default { + setupFilesAfterEnv: ['./src/test-setup.ts'], + displayName: 'test', + preset: '../../jest.preset.js', + transform: { + '^(?!.*\\\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/test', +}; +" +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=derived --unitTestRunner should create the correct config files for testing with jest 2`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=derived --unitTestRunner should create the correct config files for testing with vitest 1`] = ` +"import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/libs/test', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + setupFiles: ['./src/test-setup.ts'], + globals: true, + cache: { dir: '../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { reportsDirectory: '../../coverage/libs/test', provider: 'v8' }, + }, +}); +" +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=derived --unitTestRunner should create the correct config files for testing with vitest 2`] = ` +"import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); +" +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=derived should generate a library correctly 1`] = ` +[ + "test.module.css", + "test.spec.tsx", + "test.tsx", +] +`; + +exports[`Remix Library Generator -projectNameAndRootFormat=derived should generate a library correctly 2`] = ` +{ + "@proj/libs/test": [ + "libs/test/src/index.ts", + ], + "@proj/libs/test/server": [ + "libs/test/src/server.ts", + ], +} +`; diff --git a/packages/remix/src/generators/library/lib/add-tsconfig-entry-points.ts b/packages/remix/src/generators/library/lib/add-tsconfig-entry-points.ts new file mode 100644 index 00000000000000..f1081b544c91ff --- /dev/null +++ b/packages/remix/src/generators/library/lib/add-tsconfig-entry-points.ts @@ -0,0 +1,35 @@ +import type { Tree } from '@nx/devkit'; +import { + joinPathFragments, + readProjectConfiguration, + updateJson, +} from '@nx/devkit'; +import { getRootTsConfigPathInTree } from '@nx/js'; +import type { RemixLibraryOptions } from './normalize-options'; + +export function addTsconfigEntryPoints( + tree: Tree, + options: RemixLibraryOptions +) { + const { sourceRoot } = readProjectConfiguration(tree, options.projectName); + const serverFilePath = joinPathFragments(sourceRoot, 'server.ts'); + + tree.write( + serverFilePath, + `// This file should be used to export ONLY server-code from the library.` + ); + + const baseTsConfig = getRootTsConfigPathInTree(tree); + updateJson(tree, baseTsConfig, (json) => { + if ( + json.compilerOptions.paths && + json.compilerOptions.paths[options.importPath] + ) { + json.compilerOptions.paths[ + joinPathFragments(options.importPath, 'server') + ] = [serverFilePath]; + } + + return json; + }); +} diff --git a/packages/remix/src/generators/library/lib/add-unit-testing.ts b/packages/remix/src/generators/library/lib/add-unit-testing.ts new file mode 100644 index 00000000000000..8698227b9e2547 --- /dev/null +++ b/packages/remix/src/generators/library/lib/add-unit-testing.ts @@ -0,0 +1,62 @@ +import { + addDependenciesToPackageJson, + joinPathFragments, + stripIndents, + type Tree, +} from '@nx/devkit'; +import { + updateJestTestSetup, + updateViteTestSetup, +} from '../../../utils/testing-config-utils'; +import { + getRemixVersion, + testingLibraryJestDomVersion, + testingLibraryReactVersion, + testingLibraryUserEventsVersion, +} from '../../../utils/versions'; +import type { RemixLibraryOptions } from './normalize-options'; + +export function addUnitTestingSetup(tree: Tree, options: RemixLibraryOptions) { + const pathToTestSetup = joinPathFragments( + options.projectRoot, + 'src/test-setup.ts' + ); + let testSetupFileContents = ''; + + if (tree.exists(pathToTestSetup)) { + testSetupFileContents = tree.read(pathToTestSetup, 'utf-8'); + } + + tree.write( + pathToTestSetup, + stripIndents`${testSetupFileContents} + import { installGlobals } from '@remix-run/node'; + import "@testing-library/jest-dom/matchers"; + installGlobals();` + ); + + if (options.unitTestRunner === 'vitest') { + const pathToVitestConfig = joinPathFragments( + options.projectRoot, + `vite.config.ts` + ); + updateViteTestSetup(tree, pathToVitestConfig, './src/test-setup.ts'); + } else if (options.unitTestRunner === 'jest') { + const pathToJestConfig = joinPathFragments( + options.projectRoot, + `jest.config.ts` + ); + updateJestTestSetup(tree, pathToJestConfig, './src/test-setup.ts'); + } + + return addDependenciesToPackageJson( + tree, + {}, + { + '@testing-library/jest-dom': testingLibraryJestDomVersion, + '@testing-library/react': testingLibraryReactVersion, + '@testing-library/user-event': testingLibraryUserEventsVersion, + '@remix-run/node': getRemixVersion(tree), + } + ); +} diff --git a/packages/remix/src/generators/library/lib/index.ts b/packages/remix/src/generators/library/lib/index.ts new file mode 100644 index 00000000000000..95f271206e3855 --- /dev/null +++ b/packages/remix/src/generators/library/lib/index.ts @@ -0,0 +1,4 @@ +export * from './add-tsconfig-entry-points'; +export * from './add-unit-testing'; +export * from './normalize-options'; +export * from './update-buildable-config'; diff --git a/packages/remix/src/generators/library/lib/normalize-options.ts b/packages/remix/src/generators/library/lib/normalize-options.ts new file mode 100644 index 00000000000000..a4e1bdf3828c34 --- /dev/null +++ b/packages/remix/src/generators/library/lib/normalize-options.ts @@ -0,0 +1,33 @@ +import type { Tree } from '@nx/devkit'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import type { NxRemixGeneratorSchema } from '../schema'; + +export interface RemixLibraryOptions extends NxRemixGeneratorSchema { + projectName: string; + projectRoot: string; +} + +export async function normalizeOptions( + tree: Tree, + options: NxRemixGeneratorSchema +): Promise { + const { projectName, projectRoot, projectNameAndRootFormat } = + await determineProjectNameAndRootOptions(tree, { + name: options.name, + projectType: 'library', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/remix:library', + }); + + const importPath = options.importPath ?? getImportPath(tree, projectRoot); + + return { + ...options, + unitTestRunner: options.unitTestRunner ?? 'vitest', + importPath, + projectName, + projectRoot, + }; +} diff --git a/packages/remix/src/generators/library/lib/update-buildable-config.ts b/packages/remix/src/generators/library/lib/update-buildable-config.ts new file mode 100644 index 00000000000000..a09585395402a0 --- /dev/null +++ b/packages/remix/src/generators/library/lib/update-buildable-config.ts @@ -0,0 +1,25 @@ +import type { Tree } from '@nx/devkit'; +import { + joinPathFragments, + readProjectConfiguration, + updateJson, + updateProjectConfiguration, +} from '@nx/devkit'; + +export function updateBuildableConfig(tree: Tree, name: string) { + // Nest dist under project root to we can link it + const project = readProjectConfiguration(tree, name); + project.targets.build.options = { + ...project.targets.build.options, + format: ['cjs'], + outputPath: joinPathFragments(project.root, 'dist'), + }; + updateProjectConfiguration(tree, name, project); + + // Point to nested dist for yarn/npm/pnpm workspaces + updateJson(tree, joinPathFragments(project.root, 'package.json'), (json) => { + json.main = './dist/index.cjs.js'; + json.typings = './dist/index.d.ts'; + return json; + }); +} diff --git a/packages/remix/src/generators/library/library.impl.spec.ts b/packages/remix/src/generators/library/library.impl.spec.ts new file mode 100644 index 00000000000000..110f855e932feb --- /dev/null +++ b/packages/remix/src/generators/library/library.impl.spec.ts @@ -0,0 +1,147 @@ +import { readJson, readProjectConfiguration } from '@nx/devkit'; +import { type ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from '../application/application.impl'; +import libraryGenerator from './library.impl'; + +describe('Remix Library Generator', () => { + describe.each([ + ['derived', 'libs/test'], + ['as-provided', 'test'], + ])( + '-projectNameAndRootFormat=%s', + (projectNameAndRootFormat: ProjectNameAndRootFormat, libDir) => { + it('should generate a library correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await libraryGenerator(tree, { + name: 'test', + style: 'css', + projectNameAndRootFormat, + }); + + // ASSERT + const tsconfig = readJson(tree, 'tsconfig.base.json'); + expect(tree.exists(`${libDir}/src/server.ts`)); + expect(tree.children(`${libDir}/src/lib`)).toMatchSnapshot(); + expect(tsconfig.compilerOptions.paths).toMatchSnapshot(); + }, 25_000); + + describe('Standalone Project Repo', () => { + it('should update the tsconfig paths correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + await applicationGenerator(tree, { + name: 'demo', + rootProject: true, + }); + const originalBaseTsConfig = readJson(tree, 'tsconfig.json'); + + // ACT + await libraryGenerator(tree, { + name: 'test', + style: 'css', + projectNameAndRootFormat, + }); + + // ASSERT + const updatedBaseTsConfig = readJson(tree, 'tsconfig.base.json'); + expect( + Object.keys(originalBaseTsConfig.compilerOptions.paths) + ).toContain('~/*'); + expect( + Object.keys(updatedBaseTsConfig.compilerOptions.paths) + ).toContain('~/*'); + }); + }); + + describe('--unitTestRunner', () => { + it('should not create config files when unitTestRunner=none', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await libraryGenerator(tree, { + name: 'test', + style: 'css', + unitTestRunner: 'none', + projectNameAndRootFormat, + }); + + // ASSERT + expect(tree.exists(`${libDir}/jest.config.ts`)).toBeFalsy(); + expect(tree.exists(`${libDir}/vite.config.ts`)).toBeFalsy(); + }); + + it('should create the correct config files for testing with jest', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await libraryGenerator(tree, { + name: 'test', + style: 'css', + unitTestRunner: 'jest', + projectNameAndRootFormat, + }); + + // ASSERT + expect( + tree.read(`${libDir}/jest.config.ts`, 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read(`${libDir}/src/test-setup.ts`, 'utf-8') + ).toMatchSnapshot(); + }); + + it('should create the correct config files for testing with vitest', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await libraryGenerator(tree, { + name: 'test', + style: 'css', + unitTestRunner: 'vitest', + projectNameAndRootFormat, + }); + + // ASSERT + expect( + tree.read(`${libDir}/vite.config.ts`, 'utf-8') + ).toMatchSnapshot(); + + expect( + tree.read(`${libDir}/src/test-setup.ts`, 'utf-8') + ).toMatchSnapshot(); + }, 25_000); + }); + + // TODO(Colum): Unskip this when buildable is investigated correctly + xit('should generate the config files correctly when the library is buildable', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + // ACT + await libraryGenerator(tree, { + name: 'test', + style: 'css', + buildable: true, + projectNameAndRootFormat, + }); + + // ASSERT + const project = readProjectConfiguration(tree, 'test'); + const pkgJson = readJson(tree, `${libDir}/package.json`); + expect(project.targets.build.options.format).toEqual(['cjs']); + expect(project.targets.build.options.outputPath).toEqual( + `${libDir}/dist` + ); + expect(pkgJson.main).toEqual('./dist/index.cjs.js'); + expect(pkgJson.typings).toEqual('./dist/index.d.ts'); + }); + } + ); +}); diff --git a/packages/remix/src/generators/library/library.impl.ts b/packages/remix/src/generators/library/library.impl.ts new file mode 100644 index 00000000000000..8eebe8b36058b1 --- /dev/null +++ b/packages/remix/src/generators/library/library.impl.ts @@ -0,0 +1,49 @@ +import type { Tree } from '@nx/devkit'; +import { formatFiles, GeneratorCallback, runTasksInSerial } from '@nx/devkit'; +import { Linter } from '@nx/eslint'; +import { libraryGenerator } from '@nx/react'; +import { + addTsconfigEntryPoints, + addUnitTestingSetup, + normalizeOptions, + updateBuildableConfig, +} from './lib'; +import type { NxRemixGeneratorSchema } from './schema'; + +export default async function (tree: Tree, schema: NxRemixGeneratorSchema) { + const tasks: GeneratorCallback[] = []; + const options = await normalizeOptions(tree, schema); + + const libGenTask = await libraryGenerator(tree, { + name: options.projectName, + style: options.style, + unitTestRunner: options.unitTestRunner, + tags: options.tags, + importPath: options.importPath, + directory: options.projectRoot, + projectNameAndRootFormat: 'as-provided', + skipFormat: true, + skipTsConfig: false, + linter: Linter.EsLint, + component: true, + buildable: options.buildable, + }); + tasks.push(libGenTask); + + if (options.unitTestRunner && options.unitTestRunner !== 'none') { + const pkgInstallTask = addUnitTestingSetup(tree, options); + tasks.push(pkgInstallTask); + } + + addTsconfigEntryPoints(tree, options); + + if (options.buildable) { + updateBuildableConfig(tree, options.projectName); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return runTasksInSerial(...tasks); +} diff --git a/packages/remix/src/generators/library/schema.d.ts b/packages/remix/src/generators/library/schema.d.ts new file mode 100644 index 00000000000000..05ce3c85aedeaa --- /dev/null +++ b/packages/remix/src/generators/library/schema.d.ts @@ -0,0 +1,15 @@ +import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { SupportedStyles } from '@nx/react'; + +export interface NxRemixGeneratorSchema { + name: string; + style: SupportedStyles; + directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; + tags?: string; + importPath?: string; + buildable?: boolean; + unitTestRunner?: 'jest' | 'vitest' | 'none'; + js?: boolean; + skipFormat?: boolean; +} diff --git a/packages/remix/src/generators/library/schema.json b/packages/remix/src/generators/library/schema.json new file mode 100644 index 00000000000000..23cad10a6b053a --- /dev/null +++ b/packages/remix/src/generators/library/schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixLibrary", + "title": "Create a Library", + "type": "object", + "examples": [ + { + "command": "g lib mylib --directory=myapp", + "description": "Generate libs/myapp/mylib" + } + ], + "properties": { + "name": { + "type": "string", + "description": "Library name", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the library?", + "pattern": "^[a-zA-Z].*$" + }, + "directory": { + "type": "string", + "description": "A directory where the lib is placed.", + "alias": "dir", + "x-priority": "important" + }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "tags": { + "type": "string", + "description": "Add tags to the library (used for linting)" + }, + "style": { + "type": "string", + "description": "Generate a stylesheet", + "enum": ["none", "css"], + "default": "css" + }, + "buildable": { + "type": "boolean", + "description": "Should the library be buildable?", + "default": false + }, + "unitTestRunner": { + "type": "string", + "enum": ["jest", "vitest", "none"], + "description": "Test Runner to use for Unit Tests", + "x-prompt": "What test runner should be used?", + "default": "vitest" + }, + "importPath": { + "type": "string", + "description": "The library name used to import it, like @myorg/my-awesome-lib" + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files", + "default": false + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files after generator runs", + "default": false, + "x-priority": "internal" + } + }, + "required": ["name"] +} diff --git a/packages/remix/src/generators/loader/loader.impl.spec.ts b/packages/remix/src/generators/loader/loader.impl.spec.ts new file mode 100644 index 00000000000000..8eca53442f396a --- /dev/null +++ b/packages/remix/src/generators/loader/loader.impl.spec.ts @@ -0,0 +1,90 @@ +import { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from '../application/application.impl'; +import routeGenerator from '../route/route.impl'; +import loaderGenerator from './loader.impl'; + +describe('loader', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write('.gitignore', `/node_modules/dist`); + + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + path: 'example', + project: 'demo', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + }); + + [ + { + path: 'apps/demo/app/routes/example.tsx', + }, + { + path: 'example', + }, + { + path: 'example.tsx', + }, + ].forEach((config) => { + describe(`add loader using route path "${config.path}"`, () => { + beforeEach(async () => { + await loaderGenerator(tree, { + path: config.path, + project: 'demo', + }); + }); + + it('should add imports', async () => { + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch(`import { json } from '@remix-run/node';`); + expect(content).toMatch( + `import type { LoaderArgs } from '@remix-run/node';` + ); + expect(content).toMatch( + `import { useLoaderData } from '@remix-run/react';` + ); + }); + + it('should add loader function', () => { + const loaderFunction = `export const loader = async`; + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch(loaderFunction); + }); + + it('should add useLoaderData to component', () => { + const useLoaderData = `const data = useLoaderData();`; + + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch(useLoaderData); + }); + }); + }); + + describe('--nameAndDirectoryFormat=as-provided', () => { + it('should add imports', async () => { + // ACT + await loaderGenerator(tree, { + path: 'apps/demo/app/routes/example.tsx', + nameAndDirectoryFormat: 'as-provided', + }); + + // ASSERT + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch(`import { json } from '@remix-run/node';`); + expect(content).toMatch( + `import type { LoaderArgs } from '@remix-run/node';` + ); + expect(content).toMatch( + `import { useLoaderData } from '@remix-run/react';` + ); + }); + }); +}); diff --git a/packages/remix/src/generators/loader/loader.impl.ts b/packages/remix/src/generators/loader/loader.impl.ts new file mode 100644 index 00000000000000..9b7dacfc05c01d --- /dev/null +++ b/packages/remix/src/generators/loader/loader.impl.ts @@ -0,0 +1,48 @@ +import { formatFiles, Tree } from '@nx/devkit'; +import { insertImport } from '../../utils/insert-import'; +import { insertStatementAfterImports } from '../../utils/insert-statement-after-imports'; +import { insertStatementInDefaultFunction } from '../../utils/insert-statement-in-default-function'; +import { resolveRemixRouteFile } from '../../utils/remix-route-utils'; +import { LoaderSchema } from './schema'; + +export default async function (tree: Tree, schema: LoaderSchema) { + const routeFilePath = + schema.nameAndDirectoryFormat === 'as-provided' + ? schema.path + : await resolveRemixRouteFile(tree, schema.path, schema.project); + + if (!tree.exists(routeFilePath)) { + throw new Error( + `Route path does not exist: ${routeFilePath}. Please generate a Remix route first.` + ); + } + + insertImport(tree, routeFilePath, 'useLoaderData', '@remix-run/react'); + insertImport(tree, routeFilePath, 'json', '@remix-run/node'); + insertImport(tree, routeFilePath, 'LoaderArgs', '@remix-run/node', { + typeOnly: true, + }); + + insertStatementAfterImports( + tree, + routeFilePath, + ` + export const loader = async ({request}: LoaderArgs ) => { + return json({ + message: 'Hello, world!', + }) + }; + + ` + ); + + const statement = `\nconst data = useLoaderData();`; + + try { + insertStatementInDefaultFunction(tree, routeFilePath, statement); + // eslint-disable-next-line no-empty + } catch (err) { + } finally { + await formatFiles(tree); + } +} diff --git a/packages/remix/src/generators/loader/schema.d.ts b/packages/remix/src/generators/loader/schema.d.ts new file mode 100644 index 00000000000000..9315409e926eeb --- /dev/null +++ b/packages/remix/src/generators/loader/schema.d.ts @@ -0,0 +1,10 @@ +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +export interface LoaderSchema { + path: string; + nameAndDirectoryFormat?: NameAndDirectoryFormat; + /** + * @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18. + */ + project?: string; +} diff --git a/packages/remix/src/generators/loader/schema.json b/packages/remix/src/generators/loader/schema.json new file mode 100644 index 00000000000000..8f33929d185354 --- /dev/null +++ b/packages/remix/src/generators/loader/schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "data-loader", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the loader in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + } + }, + "required": ["path"] +} diff --git a/packages/remix/src/generators/meta/lib/v2.impl.spec.ts b/packages/remix/src/generators/meta/lib/v2.impl.spec.ts new file mode 100644 index 00000000000000..17154a0e3bb4ca --- /dev/null +++ b/packages/remix/src/generators/meta/lib/v2.impl.spec.ts @@ -0,0 +1,41 @@ +import { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from '../../application/application.impl'; +import routeGenerator from '../../route/route.impl'; +import { v2MetaGenerator } from './v2.impl'; + +describe('meta v2', () => { + let tree: Tree; + + test.each([['apps/demo/app/routes/example.tsx', 'example', 'example.tsx']])( + 'add meta using route path "%s"', + async (path) => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write('.gitignore', `/node_modules/dist`); + + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + path: 'example', + project: 'demo', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + + await v2MetaGenerator(tree, { + path, + project: 'demo', + }); + + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch( + `import type { MetaFunction } from '@remix-run/node';` + ); + + expect(content).toMatch(`export const meta: MetaFunction`); + expect(content).toMatch(`return [`); + } + ); +}); diff --git a/packages/remix/src/generators/meta/lib/v2.impl.ts b/packages/remix/src/generators/meta/lib/v2.impl.ts new file mode 100644 index 00000000000000..ede45961410b3f --- /dev/null +++ b/packages/remix/src/generators/meta/lib/v2.impl.ts @@ -0,0 +1,36 @@ +import { formatFiles, Tree } from '@nx/devkit'; +import { getDefaultExportName } from '../../../utils/get-default-export-name'; +import { insertImport } from '../../../utils/insert-import'; +import { insertStatementAfterImports } from '../../../utils/insert-statement-after-imports'; +import { resolveRemixRouteFile } from '../../../utils/remix-route-utils'; +import { MetaSchema } from '../schema'; + +export async function v2MetaGenerator(tree: Tree, schema: MetaSchema) { + const routeFilePath = + schema.nameAndDirectoryFormat === 'as-provided' + ? schema.path + : await resolveRemixRouteFile(tree, schema.path, schema.project); + + if (!tree.exists(routeFilePath)) { + throw new Error( + `Route path does not exist: ${routeFilePath}. Please generate a Remix route first.` + ); + } + + insertImport(tree, routeFilePath, 'MetaFunction', '@remix-run/node', { + typeOnly: true, + }); + + const defaultExportName = getDefaultExportName(tree, routeFilePath); + insertStatementAfterImports( + tree, + routeFilePath, + ` + export const meta: MetaFunction = () => { + return [{ title: '${defaultExportName} Route' }]; + }; + + ` + ); + await formatFiles(tree); +} diff --git a/packages/remix/src/generators/meta/meta.impl.spec.ts b/packages/remix/src/generators/meta/meta.impl.spec.ts new file mode 100644 index 00000000000000..db2159938be23d --- /dev/null +++ b/packages/remix/src/generators/meta/meta.impl.spec.ts @@ -0,0 +1,54 @@ +import { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from '../application/application.impl'; +import routeGenerator from '../route/route.impl'; +import metaGenerator from './meta.impl'; + +describe('meta', () => { + let tree: Tree; + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write('.gitignore', `/node_modules/dist`); + + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + path: 'example', + project: 'demo', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + }); + + it('should use v2 when specified', async () => { + await metaGenerator(tree, { + path: 'example', + project: 'demo', + }); + + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch( + `import type { MetaFunction } from '@remix-run/node';` + ); + + expect(content).toMatch(`export const meta: MetaFunction`); + expect(content).toMatch(`return [`); + }); + + it('--nameAndDirectoryFormat=as=provided', async () => { + await metaGenerator(tree, { + path: 'apps/demo/app/routes/example.tsx', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8'); + expect(content).toMatch( + `import type { MetaFunction } from '@remix-run/node';` + ); + + expect(content).toMatch(`export const meta: MetaFunction`); + expect(content).toMatch(`return [`); + }); +}); diff --git a/packages/remix/src/generators/meta/meta.impl.ts b/packages/remix/src/generators/meta/meta.impl.ts new file mode 100644 index 00000000000000..308edd0ca76c5d --- /dev/null +++ b/packages/remix/src/generators/meta/meta.impl.ts @@ -0,0 +1,7 @@ +import { Tree } from '@nx/devkit'; +import { v2MetaGenerator } from './lib/v2.impl'; +import { MetaSchema } from './schema'; + +export default async function (tree: Tree, schema: MetaSchema) { + await v2MetaGenerator(tree, schema); +} diff --git a/packages/remix/src/generators/meta/schema.d.ts b/packages/remix/src/generators/meta/schema.d.ts new file mode 100644 index 00000000000000..c81caf4076eda2 --- /dev/null +++ b/packages/remix/src/generators/meta/schema.d.ts @@ -0,0 +1,10 @@ +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +export interface MetaSchema { + path: string; + nameAndDirectoryFormat?: NameAndDirectoryFormat; + /** + * @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18. + */ + project?: string; +} diff --git a/packages/remix/src/generators/meta/schema.json b/packages/remix/src/generators/meta/schema.json new file mode 100644 index 00000000000000..c5d4e60513d6f1 --- /dev/null +++ b/packages/remix/src/generators/meta/schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "meta", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the meta function in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + } + }, + "required": ["path"] +} diff --git a/packages/remix/src/generators/preset/lib/normalize-options.ts b/packages/remix/src/generators/preset/lib/normalize-options.ts new file mode 100644 index 00000000000000..7ebd7f54470ee1 --- /dev/null +++ b/packages/remix/src/generators/preset/lib/normalize-options.ts @@ -0,0 +1,32 @@ +import { Tree } from '@nx/devkit'; +import { RemixGeneratorSchema } from '../schema'; + +export interface NormalizedSchema extends RemixGeneratorSchema { + appName: string; + projectRoot: string; + parsedTags: string[]; + unitTestRunner?: 'jest' | 'none' | 'vitest'; + e2eTestRunner?: 'cypress' | 'none'; + js?: boolean; +} + +export function normalizeOptions( + tree: Tree, + options: RemixGeneratorSchema +): NormalizedSchema { + // There is a bug in Nx core where custom preset args are not passed correctly for boolean values, thus causing the name to be "commit" or "nx-cloud" when not passed. + // TODO(jack): revert this hack once Nx core is fixed for custom preset args. + // TODO(philip): presets should probably be using the `appName` flag to name the app, but it's not getting passed down to this generator properly and is always an empty string + const appName = options.name; + const projectRoot = `packages/${appName}`; + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + return { + ...options, + appName, + projectRoot, + parsedTags, + }; +} diff --git a/packages/remix/src/generators/preset/preset.impl.ts b/packages/remix/src/generators/preset/preset.impl.ts new file mode 100644 index 00000000000000..a64081e3ea824c --- /dev/null +++ b/packages/remix/src/generators/preset/preset.impl.ts @@ -0,0 +1,33 @@ +import { formatFiles, GeneratorCallback, Tree } from '@nx/devkit'; + +import { runTasksInSerial } from '@nx/devkit'; +import applicationGenerator from '../application/application.impl'; +import setupGenerator from '../setup/setup.impl'; +import { normalizeOptions } from './lib/normalize-options'; +import { RemixGeneratorSchema } from './schema'; + +export default async function (tree: Tree, _options: RemixGeneratorSchema) { + const options = normalizeOptions(tree, _options); + const tasks: GeneratorCallback[] = []; + + const setupGenTask = await setupGenerator(tree); + tasks.push(setupGenTask); + + const appGenTask = await applicationGenerator(tree, { + name: options.appName, + tags: options.tags, + skipFormat: true, + rootProject: true, + unitTestRunner: options.unitTestRunner ?? 'vitest', + e2eTestRunner: options.e2eTestRunner ?? 'cypress', + js: options.js ?? false, + }); + tasks.push(appGenTask); + + tree.delete('apps'); + tree.delete('libs'); + + await formatFiles(tree); + + return runTasksInSerial(...tasks); +} diff --git a/packages/remix/src/generators/preset/schema.d.ts b/packages/remix/src/generators/preset/schema.d.ts new file mode 100644 index 00000000000000..041395c7d0409d --- /dev/null +++ b/packages/remix/src/generators/preset/schema.d.ts @@ -0,0 +1,4 @@ +export interface RemixGeneratorSchema { + name: string; + tags?: string; +} diff --git a/packages/remix/src/generators/preset/schema.json b/packages/remix/src/generators/preset/schema.json new file mode 100644 index 00000000000000..e438606b3c0cc0 --- /dev/null +++ b/packages/remix/src/generators/preset/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "Remix", + "title": "", + "type": "object", + "properties": { + "tags": { + "type": "string", + "description": "Add tags to the app (used for linting).", + "alias": "t" + } + } +} diff --git a/packages/remix/src/generators/resource-route/__snapshots__/resource-route.impl.spec.ts.snap b/packages/remix/src/generators/resource-route/__snapshots__/resource-route.impl.spec.ts.snap new file mode 100644 index 00000000000000..4b664673dd04a7 --- /dev/null +++ b/packages/remix/src/generators/resource-route/__snapshots__/resource-route.impl.spec.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resource route --nameAndDirectoryFormat=as-provided should error if it detects a possible missing route param because of un-escaped dollar sign 1`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=as-provided should error if it detects a possible missing route param because of un-escaped dollar sign 3`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=as-provided should error if it detects a possible missing route param because of un-escaped dollar sign 4`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=as-provided should error if it detects a possible missing route param because of un-escaped dollar sign 6`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 1`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 3`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 4`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 6`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 7`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`resource route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 9`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; diff --git a/packages/remix/src/generators/resource-route/resource-route.impl.spec.ts b/packages/remix/src/generators/resource-route/resource-route.impl.spec.ts new file mode 100644 index 00000000000000..a25d48b6f792e9 --- /dev/null +++ b/packages/remix/src/generators/resource-route/resource-route.impl.spec.ts @@ -0,0 +1,155 @@ +import { Tree } from '@nx/devkit'; +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { dirname } from 'path'; +import applicationGenerator from '../application/application.impl'; +import resourceRouteGenerator from './resource-route.impl'; + +describe('resource route', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write('.gitignore', `/node_modules/dist`); + + await applicationGenerator(tree, { name: 'demo' }); + }); + + it('should not create a component', async () => { + await resourceRouteGenerator(tree, { + project: 'demo', + path: '/example/', + action: false, + loader: true, + skipChecks: false, + }); + const fileContents = tree.read('apps/demo/app/routes/example.ts', 'utf-8'); + expect(fileContents).not.toMatch('export default function'); + }); + + it('should throw an error if loader and action are both false', async () => { + await expect( + async () => + await resourceRouteGenerator(tree, { + project: 'demo', + path: 'example', + action: false, + loader: false, + skipChecks: false, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"The resource route generator requires either \`loader\` or \`action\` to be true"` + ); + }); + + describe.each([ + ['derived', 'apps/demo/app/routes/example.ts', 'demo'], + ['derived', 'example', 'demo'], + ['derived', 'example.ts', 'demo'], + ['as-provided', 'apps/demo/app/routes/example', ''], + ['as-provided', 'apps/demo/app/routes/example.ts', ''], + ])( + '--nameAndDirectoryFormat=%s', + ( + nameAndDirectoryFormat: NameAndDirectoryFormat, + path: string, + project: string + ) => { + it(`should create correct file for path ${path}`, async () => { + await resourceRouteGenerator(tree, { + project, + path, + action: false, + loader: true, + skipChecks: false, + nameAndDirectoryFormat, + }); + + expect(tree.exists('apps/demo/app/routes/example.ts')).toBeTruthy(); + }); + + it('should error if it detects a possible missing route param because of un-escaped dollar sign', async () => { + expect.assertions(3); + + await resourceRouteGenerator(tree, { + project, + path: `${dirname(path)}/route1/.ts`, // route.$withParams.tsx => route..tsx + loader: true, + action: true, + skipChecks: false, + nameAndDirectoryFormat, + }).catch((e) => expect(e).toMatchSnapshot()); + + await resourceRouteGenerator(tree, { + project, + path: `${dirname(path)}/route2//index.ts`, // route/$withParams/index.tsx => route//index.tsx + loader: true, + action: true, + skipChecks: false, + nameAndDirectoryFormat, + }).catch((e) => + expect(e).toMatchInlineSnapshot( + `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]` + ) + ); + + await resourceRouteGenerator(tree, { + project, + path: `${dirname(path)}/route3/.ts`, // route/$withParams.tsx => route/.tsx + loader: true, + action: true, + skipChecks: false, + nameAndDirectoryFormat, + }).catch((e) => expect(e).toMatchSnapshot()); + }); + + it(`should succeed if skipChecks flag is passed, and it detects a possible missing route param because of un-escaped dollar sign for ${path}`, async () => { + const basePath = + nameAndDirectoryFormat === 'as-provided' + ? '' + : 'apps/demo/app/routes'; + const normalizedPath = ( + dirname(path) === '' ? '' : `${dirname(path)}/` + ).replace(basePath, ''); + await resourceRouteGenerator(tree, { + project, + path: `${normalizedPath}route1/..ts`, // route.$withParams.tsx => route..tsx + loader: true, + action: true, + skipChecks: true, + nameAndDirectoryFormat, + }); + + expect(tree.exists(`${basePath}/${normalizedPath}route1/..ts`)).toBe( + true + ); + + await resourceRouteGenerator(tree, { + project, + path: `${normalizedPath}route2//index.ts`, // route/$withParams/index.tsx => route//index.tsx + loader: true, + action: true, + skipChecks: true, + nameAndDirectoryFormat, + }); + + expect( + tree.exists(`${basePath}/${normalizedPath}route2/index.ts`) + ).toBe(true); + + await resourceRouteGenerator(tree, { + project, + path: `${normalizedPath}route3/.ts`, // route/$withParams.tsx => route/.tsx + loader: true, + action: true, + skipChecks: true, + nameAndDirectoryFormat, + }); + + expect(tree.exists(`${basePath}/${normalizedPath}route3/.ts`)).toBe( + true + ); + }); + } + ); +}); diff --git a/packages/remix/src/generators/resource-route/resource-route.impl.ts b/packages/remix/src/generators/resource-route/resource-route.impl.ts new file mode 100644 index 00000000000000..d366fb3f28f384 --- /dev/null +++ b/packages/remix/src/generators/resource-route/resource-route.impl.ts @@ -0,0 +1,64 @@ +import { formatFiles, joinPathFragments, Tree } from '@nx/devkit'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { + checkRoutePathForErrors, + resolveRemixRouteFile, +} from '../../utils/remix-route-utils'; +import actionGenerator from '../action/action.impl'; +import loaderGenerator from '../loader/loader.impl'; +import { RemixRouteSchema } from './schema'; + +export default async function (tree: Tree, options: RemixRouteSchema) { + const { + artifactName: name, + directory, + project: projectName, + } = await determineArtifactNameAndDirectoryOptions(tree, { + artifactType: 'resource-route', + callingGenerator: '@nx/remix:resource-route', + name: options.path.replace(/^\//, '').replace(/\/$/, ''), + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + }); + + if (!options.skipChecks && checkRoutePathForErrors(options.path)) { + throw new Error( + `Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.` + ); + } + + const routeFilePath = await resolveRemixRouteFile( + tree, + options.nameAndDirectoryFormat === 'as-provided' + ? joinPathFragments(directory, name) + : options.path, + options.nameAndDirectoryFormat === 'as-provided' ? undefined : projectName, + '.ts' + ); + + if (tree.exists(routeFilePath)) + throw new Error(`Path already exists: ${options.path}`); + + if (!options.loader && !options.action) + throw new Error( + 'The resource route generator requires either `loader` or `action` to be true' + ); + + tree.write(routeFilePath, ''); + + if (options.loader) { + await loaderGenerator(tree, { + path: routeFilePath, + nameAndDirectoryFormat: 'as-provided', + }); + } + + if (options.action) { + await actionGenerator(tree, { + path: routeFilePath, + nameAndDirectoryFormat: 'as-provided', + }); + } + + await formatFiles(tree); +} diff --git a/packages/remix/src/generators/resource-route/schema.d.ts b/packages/remix/src/generators/resource-route/schema.d.ts new file mode 100644 index 00000000000000..2512f88757da8c --- /dev/null +++ b/packages/remix/src/generators/resource-route/schema.d.ts @@ -0,0 +1,13 @@ +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +export interface RemixRouteSchema { + path: string; + nameAndDirectoryFormat?: NameAndDirectoryFormat; + action: boolean; + loader: boolean; + skipChecks: boolean; + /** + * @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18. + */ + project?: string; +} diff --git a/packages/remix/src/generators/resource-route/schema.json b/packages/remix/src/generators/resource-route/schema.json new file mode 100644 index 00000000000000..414c37a9e37280 --- /dev/null +++ b/packages/remix/src/generators/resource-route/schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixResourceRoute", + "title": "Create a Resource Route", + "type": "object", + "examples": [ + { + "command": "g resource-route 'path/to/page'", + "description": "Generate resource route at /path/to/page" + } + ], + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the styles in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + }, + "action": { + "type": "boolean", + "description": "Generate an action function", + "default": false + }, + "loader": { + "type": "boolean", + "description": "Generate a loader function", + "default": true + }, + "skipChecks": { + "type": "boolean", + "description": "Skip route error detection", + "default": false + } + }, + "required": ["path"] +} diff --git a/packages/remix/src/generators/route/__snapshots__/route.impl.spec.ts.snap b/packages/remix/src/generators/route/__snapshots__/route.impl.spec.ts.snap new file mode 100644 index 00000000000000..bbea56d820cf9d --- /dev/null +++ b/packages/remix/src/generators/route/__snapshots__/route.impl.spec.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`route --nameAndDirectoryFormat=as-provided should add route component 1`] = ` +"import { useLoaderData, useActionData } from '@remix-run/react'; +import { json } from '@remix-run/node'; +import type { + LoaderArgs, + MetaFunction, + ActionFunctionArgs, + LinksFunction, +} from '@remix-run/node'; + +import stylesUrl from '../../../styles/path/to/example.css'; + +export const links: LinksFunction = () => { + return [{ rel: 'stylesheet', href: stylesUrl }]; +}; + +export const action = async ({ request }: ActionFunctionArgs) => { + let formData = await request.formData(); + + return json({ message: formData.toString() }, { status: 200 }); +}; + +export const meta: MetaFunction = () => { + return [{ title: 'Example Route' }]; +}; + +export const loader = async ({ request }: LoaderArgs) => { + return json({ + message: 'Hello, world!', + }); +}; + +export default function Example() { + const actionMessage = useActionData(); + const data = useLoaderData(); + + return

Message: {data.message}

; +} +" +`; + +exports[`route --nameAndDirectoryFormat=as-provided should error if it detects a possible missing route param because of un-escaped dollar sign 1`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`route --nameAndDirectoryFormat=as-provided should error if it detects a possible missing route param because of un-escaped dollar sign 2`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`route --nameAndDirectoryFormat=as-provided should error if it detects a possible missing route param because of un-escaped dollar sign 3`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`route --nameAndDirectoryFormat=derived should add route component 1`] = ` +"import { useLoaderData, useActionData } from '@remix-run/react'; +import { json } from '@remix-run/node'; +import type { + LoaderArgs, + MetaFunction, + ActionFunctionArgs, + LinksFunction, +} from '@remix-run/node'; + +import stylesUrl from '../../../styles/path/to/example.css'; + +export const links: LinksFunction = () => { + return [{ rel: 'stylesheet', href: stylesUrl }]; +}; + +export const action = async ({ request }: ActionFunctionArgs) => { + let formData = await request.formData(); + + return json({ message: formData.toString() }, { status: 200 }); +}; + +export const meta: MetaFunction = () => { + return [{ title: 'PathToExample Route' }]; +}; + +export const loader = async ({ request }: LoaderArgs) => { + return json({ + message: 'Hello, world!', + }); +}; + +export default function PathToExample() { + const actionMessage = useActionData(); + const data = useLoaderData(); + + return

Message: {data.message}

; +} +" +`; + +exports[`route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 1`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 2`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; + +exports[`route --nameAndDirectoryFormat=derived should error if it detects a possible missing route param because of un-escaped dollar sign 3`] = `[Error: Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.]`; diff --git a/packages/remix/src/generators/route/route.impl.spec.ts b/packages/remix/src/generators/route/route.impl.spec.ts new file mode 100644 index 00000000000000..1370389bdf2ad1 --- /dev/null +++ b/packages/remix/src/generators/route/route.impl.spec.ts @@ -0,0 +1,282 @@ +import { Tree } from '@nx/devkit'; +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from '../application/application.impl'; +import presetGenerator from '../preset/preset.impl'; +import routeGenerator from './route.impl'; + +describe('route', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write('.gitignore', `/node_modules/dist`); + }); + describe.each([ + [ + 'derived', + 'path/to/example', + '', + 'apps/demo/app/routes/path/to/example.tsx', + 'apps/demo/app/styles/path/to/example.css', + 'PathToExample', + 'demo', + ], + [ + 'as-provided', + 'apps/demo/app/routes/path/to/example', + 'app/routes', + 'apps/demo/app/routes/path/to/example.tsx', + 'apps/demo/app/styles/path/to/example.css', + 'Example', + '', + ], + ])( + `--nameAndDirectoryFormat=%s`, + ( + nameAndDirectoryFormat: NameAndDirectoryFormat, + path, + standalonePath, + expectedRoutePath, + expectedStylePath, + expectedComponentName, + project: string + ) => { + it('should add route component', async () => { + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + project, + path, + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: false, + }); + + const content = tree.read(expectedRoutePath, 'utf-8'); + expect(content).toMatchSnapshot(); + expect(content).toMatch('LinksFunction'); + expect(content).toMatch(`function ${expectedComponentName}(`); + expect(tree.exists(expectedStylePath)).toBeTruthy(); + }, 25_000); + + it('should support --style=none', async () => { + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + project, + path, + nameAndDirectoryFormat, + style: 'none', + loader: true, + action: true, + meta: true, + skipChecks: false, + }); + + const content = tree.read(expectedRoutePath).toString(); + expect(content).not.toMatch('LinksFunction'); + expect(tree.exists(expectedStylePath)).toBeFalsy(); + }); + + it('should handle trailing and prefix slashes', async () => { + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + project, + path: `/${path}/`, + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: false, + }); + + const content = tree.read(expectedRoutePath).toString(); + expect(content).toMatch(`function ${expectedComponentName}(`); + }); + + it('should handle routes that end in a file', async () => { + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + project: 'demo', + path: `${path}.tsx`, + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: false, + }); + + const content = tree.read(expectedRoutePath).toString(); + expect(content).toMatch(`function ${expectedComponentName}(`); + }); + + it('should handle routes that have a param', async () => { + const componentName = + nameAndDirectoryFormat === 'as-provided' + ? 'WithParam' + : `${expectedComponentName}WithParam`; + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + project, + path: `/${path}/$withParam.tsx`, + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: false, + }); + + const content = tree + .read('apps/demo/app/routes/path/to/example/$withParam.tsx') + .toString(); + expect(content).toMatch(`function ${componentName}(`); + }); + + it('should error if it detects a possible missing route param because of un-escaped dollar sign', async () => { + await applicationGenerator(tree, { name: 'demo' }); + + expect.assertions(3); + + await routeGenerator(tree, { + project, + path: `${path}/route1/.tsx`, // route.$withParams.tsx => route..tsx + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: false, + }).catch((e) => expect(e).toMatchSnapshot()); + + await routeGenerator(tree, { + project, + path: `${path}/route2//index.tsx`, // route/$withParams/index.tsx => route//index.tsx + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: false, + }).catch((e) => expect(e).toMatchSnapshot()); + + await routeGenerator(tree, { + project: 'demo', + path: `${path}/route3/.tsx`, // route/$withParams.tsx => route/.tsx + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: false, + }).catch((e) => expect(e).toMatchSnapshot()); + }); + + it('should succeed if skipChecks flag is passed, and it detects a possible missing route param because of un-escaped dollar sign', async () => { + await applicationGenerator(tree, { name: 'demo' }); + + await routeGenerator(tree, { + project, + path: `${path}/route1/..tsx`, // route.$withParams.tsx => route..tsx + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: true, + }); + + expect( + tree.exists('apps/demo/app/routes/path/to/example/route1/..tsx') + ).toBe(true); + + await routeGenerator(tree, { + project, + path: `${path}/route2//index.tsx`, // route/$withParams/index.tsx => route//index.tsx + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: true, + }); + + expect( + tree.exists('apps/demo/app/routes/path/to/example/route2/index.tsx') + ).toBe(true); + + await routeGenerator(tree, { + project, + path: `${path}/route3/.tsx`, // route/$withParams.tsx => route/.tsx + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: true, + }); + + expect( + tree.exists('apps/demo/app/routes/path/to/example/route3/.tsx') + ).toBe(true); + }, 120000); + + if (nameAndDirectoryFormat === 'derived') { + it('should place routes correctly when app dir is changed', async () => { + await applicationGenerator(tree, { name: 'demo' }); + + tree.write( + 'apps/demo/remix.config.cjs', + ` + /** + * @type {import('@remix-run/dev').AppConfig} + */ + module.exports = { + ignoredRouteFiles: ["**/.*"], + appDirectory: "my-custom-dir", + };` + ); + + await routeGenerator(tree, { + project: 'demo', + path: 'route.tsx', + nameAndDirectoryFormat, + style: 'css', + loader: true, + action: true, + meta: true, + skipChecks: false, + }); + + expect(tree.exists('apps/demo/my-custom-dir/routes/route.tsx')).toBe( + true + ); + expect(tree.exists('apps/demo/my-custom-dir/styles/route.css')).toBe( + true + ); + }); + } + + it('should place the route correctly in a standalone app', async () => { + await presetGenerator(tree, { name: 'demo' }); + + await routeGenerator(tree, { + project, + path: `${standalonePath}/route.tsx`, + nameAndDirectoryFormat, + style: 'none', + loader: true, + action: true, + meta: true, + skipChecks: false, + }); + + expect(tree.exists('app/routes/route.tsx')).toBe(true); + }); + } + ); +}); diff --git a/packages/remix/src/generators/route/route.impl.ts b/packages/remix/src/generators/route/route.impl.ts new file mode 100644 index 00000000000000..fa55c8d2d84d4b --- /dev/null +++ b/packages/remix/src/generators/route/route.impl.ts @@ -0,0 +1,116 @@ +import { + formatFiles, + joinPathFragments, + names, + readProjectConfiguration, + stripIndents, + Tree, +} from '@nx/devkit'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { basename, dirname } from 'path'; +import { + checkRoutePathForErrors, + resolveRemixRouteFile, +} from '../../utils/remix-route-utils'; +import ActionGenerator from '../action/action.impl'; +import LoaderGenerator from '../loader/loader.impl'; +import MetaGenerator from '../meta/meta.impl'; +import StyleGenerator from '../style/style.impl'; +import { RemixRouteSchema } from './schema'; + +export default async function (tree: Tree, options: RemixRouteSchema) { + const { + artifactName: name, + directory, + project: projectName, + } = await determineArtifactNameAndDirectoryOptions(tree, { + artifactType: 'route', + callingGenerator: '@nx/remix:route', + name: options.path.replace(/^\//, '').replace(/\/$/, ''), + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + }); + + const project = readProjectConfiguration(tree, projectName); + if (!project) throw new Error(`Project does not exist: ${projectName}`); + + if (!options.skipChecks && checkRoutePathForErrors(options.path)) { + throw new Error( + `Your route path has an indicator of an un-escaped dollar sign for a route param. If this was intended, include the --skipChecks flag.` + ); + } + + const routeFilePath = await resolveRemixRouteFile( + tree, + options.nameAndDirectoryFormat === 'as-provided' + ? joinPathFragments(directory, name) + : options.path, + options.nameAndDirectoryFormat === 'as-provided' ? undefined : projectName, + '.tsx' + ); + + const nameToUseForComponent = + options.nameAndDirectoryFormat === 'as-provided' + ? name.replace('.tsx', '') + : options.path.replace(/^\//, '').replace(/\/$/, '').replace('.tsx', ''); + + const { className: componentName } = names( + nameToUseForComponent === '.' || nameToUseForComponent === '' + ? basename(dirname(routeFilePath)) + : nameToUseForComponent + ); + + if (tree.exists(routeFilePath)) + throw new Error(`Path already exists: ${routeFilePath}`); + + tree.write( + routeFilePath, + stripIndents` + + + export default function ${componentName}() { + ${ + options.loader + ? ` + return ( +

+ Message: {data.message} +

+ ); + ` + : `return (

${componentName} works!

)` + } + } + ` + ); + + if (options.loader) { + await LoaderGenerator(tree, { + path: routeFilePath, + nameAndDirectoryFormat: 'as-provided', + }); + } + + if (options.meta) { + await MetaGenerator(tree, { + path: routeFilePath, + nameAndDirectoryFormat: 'as-provided', + }); + } + + if (options.action) { + await ActionGenerator(tree, { + path: routeFilePath, + nameAndDirectoryFormat: 'as-provided', + }); + } + + if (options.style === 'css') { + await StyleGenerator(tree, { + project: projectName, + path: routeFilePath, + }); + } + + await formatFiles(tree); +} diff --git a/packages/remix/src/generators/route/schema.d.ts b/packages/remix/src/generators/route/schema.d.ts new file mode 100644 index 00000000000000..b94691c6bda849 --- /dev/null +++ b/packages/remix/src/generators/route/schema.d.ts @@ -0,0 +1,15 @@ +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +export interface RemixRouteSchema { + path: string; + nameAndDirectoryFormat?: NameAndDirectoryFormat; + style: 'css' | 'none'; + action: boolean; + meta: boolean; + loader: boolean; + skipChecks: boolean; + /** + * @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18. + */ + project?: string; +} diff --git a/packages/remix/src/generators/route/schema.json b/packages/remix/src/generators/route/schema.json new file mode 100644 index 00000000000000..242b633955468c --- /dev/null +++ b/packages/remix/src/generators/route/schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixRoute", + "title": "Create a Route", + "type": "object", + "examples": [ + { + "command": "g route 'path/to/page'", + "description": "Generate route at /path/to/page" + } + ], + "properties": { + "path": { + "type": "string", + "description": "The route path or path to the filename of the route. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the workspace root.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the route in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and path relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What project is this route for?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + }, + "style": { + "type": "string", + "description": "Generate a stylesheet", + "enum": ["none", "css"], + "default": "css" + }, + "meta": { + "type": "boolean", + "description": "Generate a meta function", + "default": false + }, + "action": { + "type": "boolean", + "description": "Generate an action function", + "default": false + }, + "loader": { + "type": "boolean", + "description": "Generate a loader function", + "default": false + }, + "skipChecks": { + "type": "boolean", + "description": "Skip route error detection", + "default": false + } + }, + "required": ["path"] +} diff --git a/packages/remix/src/generators/setup-tailwind/__snapshots__/setup-tailwind.impl.spec.ts.snap b/packages/remix/src/generators/setup-tailwind/__snapshots__/setup-tailwind.impl.spec.ts.snap new file mode 100644 index 00000000000000..18e26fdb54187f --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/__snapshots__/setup-tailwind.impl.spec.ts.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setup-tailwind generator should add a js tailwind config to an application correctly 1`] = ` +"import { createGlobPatternsForDependencies } from '@nx/react/tailwind'; +export default { + content: [ + './app/**/*.{js,jsx,ts,tsx}', + ...createGlobPatternsForDependencies(__dirname), + ], + theme: { + extend: {}, + }, + plugins: [], +}; +" +`; + +exports[`setup-tailwind generator should add a js tailwind config to an application correctly 2`] = ` +"@tailwind base; +@tailwind components; +@tailwind utilities; +" +`; + +exports[`setup-tailwind generator should add a js tailwind config to an application correctly 3`] = ` +"import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; +import styles from './tailwind.css'; +export const links = () => [{ rel: 'stylesheet', href: styles }]; +export const meta = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`setup-tailwind generator should add a js tailwind config to an application correctly 4`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + tailwind: true, + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; + +exports[`setup-tailwind generator should add a tailwind config to an application correctly 1`] = ` +"import type { Config } from "tailwindcss"; +import { createGlobPatternsForDependencies } from '@nx/react/tailwind'; + +export default { + content: [ + "./app/**/*.{js,jsx,ts,tsx}", + ...createGlobPatternsForDependencies(__dirname) + ], + theme: { + extend: {}, + }, + plugins: [], +} satisfies Config; +" +`; + +exports[`setup-tailwind generator should add a tailwind config to an application correctly 2`] = ` +"@tailwind base; +@tailwind components; +@tailwind utilities; +" +`; + +exports[`setup-tailwind generator should add a tailwind config to an application correctly 3`] = ` +"import type { MetaFunction, LinksFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from '@remix-run/react'; +import styles from './tailwind.css'; +export const links: LinksFunction = () => [{ rel: 'stylesheet', href: styles }]; + +export const meta: MetaFunction = () => [ + { + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', + }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} +" +`; + +exports[`setup-tailwind generator should add a tailwind config to an application correctly 4`] = ` +"/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + tailwind: true, + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +" +`; diff --git a/packages/remix/src/generators/setup-tailwind/files/app/tailwind.css__tpl__ b/packages/remix/src/generators/setup-tailwind/files/app/tailwind.css__tpl__ new file mode 100644 index 00000000000000..b5c61c956711f9 --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/files/app/tailwind.css__tpl__ @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/packages/remix/src/generators/setup-tailwind/files/tailwind.config.ts__tpl__ b/packages/remix/src/generators/setup-tailwind/files/tailwind.config.ts__tpl__ new file mode 100644 index 00000000000000..14bac87243ba17 --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/files/tailwind.config.ts__tpl__ @@ -0,0 +1,13 @@ +import type { Config } from "tailwindcss"; +import { createGlobPatternsForDependencies } from '@nx/react/tailwind'; + +export default { + content: [ + "./app/**/*.{js,jsx,ts,tsx}", + ...createGlobPatternsForDependencies(__dirname) + ], + theme: { + extend: {}, + }, + plugins: [], +} satisfies Config; diff --git a/packages/remix/src/generators/setup-tailwind/lib/index.ts b/packages/remix/src/generators/setup-tailwind/lib/index.ts new file mode 100644 index 00000000000000..f614834a1bba88 --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/lib/index.ts @@ -0,0 +1 @@ +export * from './update-remix-config'; diff --git a/packages/remix/src/generators/setup-tailwind/lib/update-remix-config.spec.ts b/packages/remix/src/generators/setup-tailwind/lib/update-remix-config.spec.ts new file mode 100644 index 00000000000000..5d9fb29d87de1d --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/lib/update-remix-config.spec.ts @@ -0,0 +1,79 @@ +import { stripIndents } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { updateRemixConfig } from './update-remix-config'; + +describe('updateRemixConfig', () => { + it('should add tailwind property to an existing config that doesnt have it', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write( + `remix.config.cjs`, + stripIndents`module.exports = { + ignoredRouteFiles: ['**/.*'], + watchPaths: ['../../libs'] + };` + ); + + // ACT + updateRemixConfig(tree, '.'); + + // ASSERT + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchInlineSnapshot(` + "module.exports = { + tailwind: true, + ignoredRouteFiles: ['**/.*'], + watchPaths: ['../../libs'] + };" + `); + }); + + it('should update tailwind property if the config has it and set to false', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write( + `remix.config.cjs`, + stripIndents`module.exports = { + ignoredRouteFiles: ['**/.*'], + tailwind: false, + watchPaths: ['../../libs'] + };` + ); + + // ACT + updateRemixConfig(tree, '.'); + + // ASSERT + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchInlineSnapshot(` + "module.exports = { + ignoredRouteFiles: ['**/.*'], + tailwind: true, + watchPaths: ['../../libs'] + };" + `); + }); + + it('should not update tailwind property if the config has it and set to true', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write( + `remix.config.cjs`, + stripIndents`module.exports = { + ignoredRouteFiles: ['**/.*'], + tailwind: true, + watchPaths: ['../../libs'] + };` + ); + + // ACT + updateRemixConfig(tree, '.'); + + // ASSERT + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchInlineSnapshot(` + "module.exports = { + ignoredRouteFiles: ['**/.*'], + tailwind: true, + watchPaths: ['../../libs'] + };" + `); + }); +}); diff --git a/packages/remix/src/generators/setup-tailwind/lib/update-remix-config.ts b/packages/remix/src/generators/setup-tailwind/lib/update-remix-config.ts new file mode 100644 index 00000000000000..e1fda1aab320af --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/lib/update-remix-config.ts @@ -0,0 +1,52 @@ +import { joinPathFragments, type Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export function updateRemixConfig(tree: Tree, projectRoot: string) { + const pathToRemixConfig = joinPathFragments(projectRoot, 'remix.config.cjs'); + + if (!tree.exists(pathToRemixConfig)) { + throw new Error( + `Could not find "${pathToRemixConfig}". Please ensure a "remix.config.cjs" exists at the root of your project.` + ); + } + + const fileContents = tree.read(pathToRemixConfig, 'utf-8'); + + const REMIX_CONFIG_OBJECT_SELECTOR = + 'PropertyAccessExpression:has(Identifier[name=module], Identifier[name=exports])~ObjectLiteralExpression'; + const ast = tsquery.ast(fileContents); + + const nodes = tsquery(ast, REMIX_CONFIG_OBJECT_SELECTOR, { + visitAllChildren: true, + }); + if (nodes.length === 0) { + throw new Error(`Remix Config is not valid, unable to update the file.`); + } + + const configObjectNode = nodes[0]; + + const propertyNodes = tsquery(configObjectNode, 'PropertyAssignment', { + visitAllChildren: true, + }); + + for (const propertyNode of propertyNodes) { + const nodeText = propertyNode.getText(); + if (nodeText.includes('tailwind') && nodeText.includes('true')) { + return; + } else if (nodeText.includes('tailwind') && nodeText.includes('false')) { + const updatedFileContents = `${fileContents.slice( + 0, + propertyNode.getStart() + )}tailwind: true${fileContents.slice(propertyNode.getEnd())}`; + tree.write(pathToRemixConfig, updatedFileContents); + return; + } + } + + const updatedFileContents = `${fileContents.slice( + 0, + configObjectNode.getStart() + 1 + )}\ntailwind: true,${fileContents.slice(configObjectNode.getStart() + 1)}`; + + tree.write(pathToRemixConfig, updatedFileContents); +} diff --git a/packages/remix/src/generators/setup-tailwind/schema.d.ts b/packages/remix/src/generators/setup-tailwind/schema.d.ts new file mode 100644 index 00000000000000..2a098d99ba4166 --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/schema.d.ts @@ -0,0 +1,5 @@ +export interface SetupTailwindSchema { + project: string; + js?: boolean; + skipFormat?: boolean; +} diff --git a/packages/remix/src/generators/setup-tailwind/schema.json b/packages/remix/src/generators/setup-tailwind/schema.json new file mode 100644 index 00000000000000..b1b98d9d4b2a2c --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixTailwind", + "title": "Add TailwindCSS to a Remix App", + "type": "object", + "examples": [ + { + "command": "g setup-tailwind --project=myapp", + "description": "Generate a TailwindCSS config for your Remix app" + } + ], + "properties": { + "project": { + "type": "string", + "description": "The name of the project to add tailwind to", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What project would you like to add Tailwind to?", + "pattern": "^[a-zA-Z].*$" + }, + "js": { + "type": "boolean", + "description": "Generate a JavaScript config file instead of a TypeScript config file", + "default": false + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files after generator runs", + "default": false, + "x-priority": "internal" + } + }, + "required": ["project"] +} diff --git a/packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.spec.ts b/packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.spec.ts new file mode 100644 index 00000000000000..3be07313c0457a --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.spec.ts @@ -0,0 +1,53 @@ +import { readJson } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from '../application/application.impl'; +import setupTailwind from './setup-tailwind.impl'; + +describe('setup-tailwind generator', () => { + it('should add a tailwind config to an application correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + await applicationGenerator(tree, { + name: 'test', + rootProject: true, + }); + + // ACT + await setupTailwind(tree, { project: 'test' }); + + // ASSERT + expect(tree.exists('tailwind.config.ts')).toBeTruthy(); + expect(tree.read('tailwind.config.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('app/tailwind.css')).toBeTruthy(); + expect(tree.read('app/tailwind.css', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app/root.tsx', 'utf-8')).toMatchSnapshot(); + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot(); + expect( + readJson(tree, 'package.json').dependencies['tailwindcss'] + ).toBeTruthy(); + }); + + it('should add a js tailwind config to an application correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + await applicationGenerator(tree, { + name: 'test', + js: true, + rootProject: true, + }); + + // ACT + await setupTailwind(tree, { project: 'test', js: true }); + + // ASSERT + expect(tree.exists('tailwind.config.js')).toBeTruthy(); + expect(tree.read('tailwind.config.js', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('app/tailwind.css')).toBeTruthy(); + expect(tree.read('app/tailwind.css', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app/root.js', 'utf-8')).toMatchSnapshot(); + expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot(); + expect( + readJson(tree, 'package.json').dependencies['tailwindcss'] + ).toBeTruthy(); + }); +}); diff --git a/packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.ts b/packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.ts new file mode 100644 index 00000000000000..bf53eee653a1dd --- /dev/null +++ b/packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.ts @@ -0,0 +1,68 @@ +import { + addDependenciesToPackageJson, + formatFiles, + generateFiles, + installPackagesTask, + joinPathFragments, + readProjectConfiguration, + toJS, + type Tree, +} from '@nx/devkit'; + +import { upsertLinksFunction } from '../../utils/upsert-links-function'; +import { tailwindVersion } from '../../utils/versions'; +import { updateRemixConfig } from './lib'; +import type { SetupTailwindSchema } from './schema'; + +export default async function setupTailwind( + tree: Tree, + options: SetupTailwindSchema +) { + const project = readProjectConfiguration(tree, options.project); + if (project.projectType !== 'application') { + throw new Error( + `Project "${options.project}" is not an application. Please ensure the project is an application.` + ); + } + + updateRemixConfig(tree, project.root); + + generateFiles(tree, joinPathFragments(__dirname, 'files'), project.root, { + tpl: '', + }); + + if (options.js) { + tree.rename( + joinPathFragments(project.root, 'app/root.js'), + joinPathFragments(project.root, 'app/root.tsx') + ); + } + const pathToRoot = joinPathFragments(project.root, 'app/root.tsx'); + upsertLinksFunction( + tree, + pathToRoot, + 'styles', + './tailwind.css', + `{ rel: "stylesheet", href: styles }` + ); + + addDependenciesToPackageJson( + tree, + { + tailwindcss: tailwindVersion, + }, + {} + ); + + if (options.js) { + toJS(tree); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return () => { + installPackagesTask(tree); + }; +} diff --git a/packages/remix/src/generators/setup/schema.json b/packages/remix/src/generators/setup/schema.json new file mode 100644 index 00000000000000..3a8d9c3c093494 --- /dev/null +++ b/packages/remix/src/generators/setup/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixSetup", + "title": "", + "type": "object", + "properties": { + "packageManager": { + "type": "string", + "description": "The package manager to setup for", + "enum": ["yarn", "npm", "pnpm"] + } + } +} diff --git a/packages/remix/src/generators/setup/setup.impl.spec.ts b/packages/remix/src/generators/setup/setup.impl.spec.ts new file mode 100644 index 00000000000000..f94c9c3c0937ee --- /dev/null +++ b/packages/remix/src/generators/setup/setup.impl.spec.ts @@ -0,0 +1,30 @@ +import { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import setupGenerator from './setup.impl'; + +describe('app', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write( + '.gitignore', + `/node_modules +/dist` + ); + }); + + it('should update ignore file', async () => { + // Idempotency + await setupGenerator(tree); + await setupGenerator(tree); + + const ignoreFile = tree.read('.gitignore').toString(); + expect(ignoreFile).toEqual(`node_modules +dist +# Remix files +apps/**/build +apps/**/.cache + `); + }); +}); diff --git a/packages/remix/src/generators/setup/setup.impl.ts b/packages/remix/src/generators/setup/setup.impl.ts new file mode 100644 index 00000000000000..e90bf0ed1a69e4 --- /dev/null +++ b/packages/remix/src/generators/setup/setup.impl.ts @@ -0,0 +1,43 @@ +import { + formatFiles, + GeneratorCallback, + runTasksInSerial, + Tree, + updateJson, +} from '@nx/devkit'; +import { initGenerator as jsInitGenerator } from '@nx/js'; + +export default async function (tree: Tree) { + const tasks: GeneratorCallback[] = []; + + const jsInitTask = await jsInitGenerator(tree, { + skipFormat: true, + }); + tasks.push(jsInitTask); + + // Ignore nested project files + let ignoreFile = tree.read('.gitignore').toString(); + if (ignoreFile.indexOf('/dist') !== -1) { + ignoreFile = ignoreFile.replace('/dist', 'dist'); + } + if (ignoreFile.indexOf('/node_modules') !== -1) { + ignoreFile = ignoreFile.replace('/node_modules', 'node_modules'); + } + if (ignoreFile.indexOf('# Remix files') === -1) { + ignoreFile = `${ignoreFile} +# Remix files +apps/**/build +apps/**/.cache + `; + } + tree.write('.gitignore', ignoreFile); + + updateJson(tree, `package.json`, (json) => { + json.type = 'module'; + return json; + }); + + await formatFiles(tree); + + return runTasksInSerial(...tasks); +} diff --git a/packages/remix/src/generators/storybook-configuration/__snapshots__/storybook-configuration.impl.spec.ts.snap b/packages/remix/src/generators/storybook-configuration/__snapshots__/storybook-configuration.impl.spec.ts.snap new file mode 100644 index 00000000000000..f79f75077640ff --- /dev/null +++ b/packages/remix/src/generators/storybook-configuration/__snapshots__/storybook-configuration.impl.spec.ts.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storybook Configuration it should create a storybook configuration and use react-vite framework with testing framework jest 1`] = ` +"import type { StorybookConfig } from '@storybook/react-vite'; + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { mergeConfig } from 'vite'; + +const config: StorybookConfig = { + stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, + + viteFinal: async (config) => + mergeConfig(config, { + plugins: [nxViteTsPaths()], + }), +}; + +export default config; + +// To customize your Vite configuration you can use the viteFinal field. +// Check https://storybook.js.org/docs/react/builders/vite#configuration +// and https://nx.dev/recipes/storybook/custom-builder-configs +" +`; + +exports[`Storybook Configuration it should create a storybook configuration and use react-vite framework with testing framework none 1`] = ` +"import type { StorybookConfig } from '@storybook/react-vite'; + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { mergeConfig } from 'vite'; + +const config: StorybookConfig = { + stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, + + viteFinal: async (config) => + mergeConfig(config, { + plugins: [nxViteTsPaths()], + }), +}; + +export default config; + +// To customize your Vite configuration you can use the viteFinal field. +// Check https://storybook.js.org/docs/react/builders/vite#configuration +// and https://nx.dev/recipes/storybook/custom-builder-configs +" +`; + +exports[`Storybook Configuration it should create a storybook configuration and use react-vite framework with testing framework vitest 1`] = ` +"import type { StorybookConfig } from '@storybook/react-vite'; + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { mergeConfig } from 'vite'; + +const config: StorybookConfig = { + stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, + + viteFinal: async (config) => + mergeConfig(config, { + plugins: [nxViteTsPaths()], + }), +}; + +export default config; + +// To customize your Vite configuration you can use the viteFinal field. +// Check https://storybook.js.org/docs/react/builders/vite#configuration +// and https://nx.dev/recipes/storybook/custom-builder-configs +" +`; diff --git a/packages/remix/src/generators/storybook-configuration/files/vite.config.ts__tpl__ b/packages/remix/src/generators/storybook-configuration/files/vite.config.ts__tpl__ new file mode 100644 index 00000000000000..5cc91668a07989 --- /dev/null +++ b/packages/remix/src/generators/storybook-configuration/files/vite.config.ts__tpl__ @@ -0,0 +1,15 @@ +/// +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; +import viteTsConfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + cacheDir: '../../../node_modules/.vite/storybook-generator-test', + + plugins: [ + react(), + viteTsConfigPaths({ + root: '../../../', + }), + ], +}) diff --git a/packages/remix/src/generators/storybook-configuration/schema.d.ts b/packages/remix/src/generators/storybook-configuration/schema.d.ts new file mode 100644 index 00000000000000..67855b1ee67f96 --- /dev/null +++ b/packages/remix/src/generators/storybook-configuration/schema.d.ts @@ -0,0 +1,15 @@ +import { Linter } from '@nx/eslint'; + +export interface StorybookConfigurationSchema { + project: string; + configureCypress: boolean; + generateStories?: boolean; + generateCypressSpecs?: boolean; + js?: boolean; + tsConfiguration?: boolean; + linter?: Linter; + cypressDirectory?: string; + ignorePaths?: string[]; + configureTestRunner?: boolean; + configureStaticServe?: boolean; +} diff --git a/packages/remix/src/generators/storybook-configuration/schema.json b/packages/remix/src/generators/storybook-configuration/schema.json new file mode 100644 index 00000000000000..933b6f16aee518 --- /dev/null +++ b/packages/remix/src/generators/storybook-configuration/schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "NxRemixStorybookConfigure", + "title": "Remix Storybook Configuration", + "description": "Set up Storybook for a Remix library.", + "type": "object", + "properties": { + "project": { + "type": "string", + "aliases": ["name", "projectName"], + "description": "Project for which to generate Storybook configuration.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "For which project do you want to generate Storybook configuration?", + "x-dropdown": "projects", + "x-priority": "important" + }, + "configureCypress": { + "type": "boolean", + "description": "Run the cypress-configure generator.", + "x-prompt": "Configure a cypress e2e app to run against the storybook instance?", + "default": true, + "x-priority": "important" + }, + "generateStories": { + "type": "boolean", + "description": "Automatically generate `*.stories.ts` files for components declared in this project?", + "x-prompt": "Automatically generate *.stories.ts files for components declared in this project?", + "default": true, + "x-priority": "important" + }, + "generateCypressSpecs": { + "type": "boolean", + "description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.", + "x-prompt": "Automatically generate test files in the Cypress E2E app generated by the cypress-configure generator?", + "default": true, + "x-priority": "important" + }, + "configureStaticServe": { + "type": "boolean", + "description": "Specifies whether to configure a static file server target for serving storybook. Helpful for speeding up CI build/test times.", + "x-prompt": "Configure a static file server for the storybook instance?", + "default": true, + "x-priority": "important" + }, + "cypressDirectory": { + "type": "string", + "description": "A directory where the Cypress project will be placed. Placed at the root by default." + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript story files rather than TypeScript story files.", + "default": false + }, + "tsConfiguration": { + "type": "boolean", + "description": "Configure your project with TypeScript. Generate main.ts and preview.ts files, instead of main.js and preview.js.", + "default": false + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint"], + "default": "eslint" + }, + "ignorePaths": { + "type": "array", + "description": "Paths to ignore when looking for components.", + "items": { + "type": "string", + "description": "Path to ignore." + }, + "examples": [ + "**/**/src/**/not-stories/**", + "libs/my-lib/**/*.something.ts", + "**/**/src/**/*.other.*", + "libs/my-lib/src/not-stories/**,**/**/src/**/*.other.*,apps/my-app/**/*.something.ts" + ] + }, + "configureTestRunner": { + "type": "boolean", + "description": "Add a Storybook Test-Runner target." + } + }, + "required": ["name"] +} diff --git a/packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.spec.ts b/packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.spec.ts new file mode 100644 index 00000000000000..e2395527c0af85 --- /dev/null +++ b/packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.spec.ts @@ -0,0 +1,33 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import libraryGenerator from '../library/library.impl'; +import storybookConfigurationGenerator from './storybook-configuration.impl'; + +describe('Storybook Configuration', () => { + it.each(['jest', 'vitest', 'none'])( + 'it should create a storybook configuration and use react-vite framework with testing framework %s', + async (unitTestRunner: 'jest' | 'vitest' | 'none') => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + await libraryGenerator(tree, { + name: 'storybook-test', + style: 'css', + unitTestRunner, + }); + + // ACT + await storybookConfigurationGenerator(tree, { + project: 'storybook-test', + configureCypress: false, + configureStaticServe: false, + generateStories: true, + }); + + // ASSERT + expect(tree.exists(`libs/storybook-test/vite.config.ts`)); + expect( + tree.read(`libs/storybook-test/.storybook/main.ts`, 'utf-8') + ).toMatchSnapshot(); + } + ); +}); diff --git a/packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.ts b/packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.ts new file mode 100644 index 00000000000000..e5a27fd4ad0dfd --- /dev/null +++ b/packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.ts @@ -0,0 +1,24 @@ +import { + generateFiles, + joinPathFragments, + readProjectConfiguration, + type Tree, +} from '@nx/devkit'; +import { join } from 'path'; +import type { StorybookConfigurationSchema } from './schema'; +import { storybookConfigurationGenerator } from '@nx/react'; + +export default async function remixStorybookConfiguration( + tree: Tree, + schema: StorybookConfigurationSchema +) { + const { root } = readProjectConfiguration(tree, schema.project); + + if (!tree.exists(joinPathFragments(root, 'vite.config.ts'))) { + generateFiles(tree, join(__dirname, 'files'), root, { tpl: '' }); + } + + const task = await storybookConfigurationGenerator(tree, schema); + + return task; +} diff --git a/packages/remix/src/generators/style/schema.d.ts b/packages/remix/src/generators/style/schema.d.ts new file mode 100644 index 00000000000000..b0bb7d20a86abb --- /dev/null +++ b/packages/remix/src/generators/style/schema.d.ts @@ -0,0 +1,10 @@ +import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +export interface RemixStyleSchema { + path: string; + nameAndDirectoryFormat?: NameAndDirectoryFormat; + /** + * @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18. + */ + project?: string; +} diff --git a/packages/remix/src/generators/style/schema.json b/packages/remix/src/generators/style/schema.json new file mode 100644 index 00000000000000..bd30d5a5ea0ed3 --- /dev/null +++ b/packages/remix/src/generators/style/schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxRemixRouteStyle", + "title": "Add style import to a route", + "type": "object", + "examples": [ + { + "command": "g style --path='apps/demo/app/routes/path/to/page.tsx'", + "description": "Generate route at apps/demo/app/routes/path/to/page.tsx" + } + ], + "properties": { + "path": { + "type": "string", + "description": "Route path", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the styles in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What project is this route in?", + "pattern": "^[a-zA-Z].*$", + "x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18." + } + }, + "required": ["path"] +} diff --git a/packages/remix/src/generators/style/style.impl.spec.ts b/packages/remix/src/generators/style/style.impl.spec.ts new file mode 100644 index 00000000000000..d2e0a48cf2bf46 --- /dev/null +++ b/packages/remix/src/generators/style/style.impl.spec.ts @@ -0,0 +1,163 @@ +import { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import applicationGenerator from '../application/application.impl'; +import presetGenerator from '../preset/preset.impl'; +import routeGenerator from '../route/route.impl'; +import styleGenerator from './style.impl'; + +describe('route', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write('.gitignore', `/node_modules/dist`); + }); + + it('should add css file to shared styles directory', async () => { + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + project: 'demo', + path: 'path/to/example', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + await styleGenerator(tree, { + project: 'demo', + path: 'path/to/example', + }); + + expect( + tree.exists('apps/demo/app/styles/path/to/example.css') + ).toBeTruthy(); + }); + + it('should handle routes that have a param', async () => { + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + project: 'demo', + path: '/example/$withParam.tsx', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + await styleGenerator(tree, { + project: 'demo', + path: '/example/$withParam.tsx', + }); + + expect( + tree.exists('apps/demo/app/styles/example/$withParam.css') + ).toBeTruthy(); + }); + + it('should place styles correctly when app dir is changed', async () => { + await applicationGenerator(tree, { name: 'demo' }); + + tree.write( + 'apps/demo/remix.config.cjs', + ` + /** + * @type {import('@remix-run/dev').AppConfig} + */ + module.exports = { + ignoredRouteFiles: ["**/.*"], + appDirectory: "my-custom-dir", + };` + ); + + await routeGenerator(tree, { + project: 'demo', + path: 'route.tsx', + style: 'none', + loader: true, + action: true, + meta: true, + skipChecks: false, + }); + await styleGenerator(tree, { + project: 'demo', + path: '/route.tsx', + }); + + expect(tree.exists('apps/demo/my-custom-dir/styles/route.css')).toBe(true); + }); + + it('should import stylesheet with a relative path in an integrated workspace', async () => { + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + project: 'demo', + path: '/example/$withParam.tsx', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + await styleGenerator(tree, { + project: 'demo', + path: '/example/$withParam.tsx', + }); + const content = tree.read( + 'apps/demo/app/routes/example/$withParam.tsx', + 'utf-8' + ); + + expect(content).toMatch( + "import stylesUrl from '../../styles/example/$withParam.css';" + ); + }); + + it('should import stylesheet using ~ in a standalone project', async () => { + await presetGenerator(tree, { name: 'demo' }); + + await routeGenerator(tree, { + project: 'demo', + path: '/example/$withParam.tsx', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + + await styleGenerator(tree, { + project: 'demo', + path: '/example/$withParam.tsx', + }); + const content = tree.read('app/routes/example/$withParam.tsx', 'utf-8'); + + expect(content).toMatch( + "import stylesUrl from '~/styles/example/$withParam.css';" + ); + }); + + it('--nameAndDirectoryFormat=as-provided', async () => { + await applicationGenerator(tree, { name: 'demo' }); + await routeGenerator(tree, { + path: 'apps/demo/app/routes/example/$withParam.tsx', + nameAndDirectoryFormat: 'as-provided', + style: 'none', + loader: false, + action: false, + meta: false, + skipChecks: false, + }); + await styleGenerator(tree, { + path: 'apps/demo/app/routes/example/$withParam.tsx', + nameAndDirectoryFormat: 'as-provided', + }); + const content = tree.read( + 'apps/demo/app/routes/example/$withParam.tsx', + 'utf-8' + ); + + expect(content).toMatch( + "import stylesUrl from '../../styles/example/$withParam.css';" + ); + }); +}); diff --git a/packages/remix/src/generators/style/style.impl.ts b/packages/remix/src/generators/style/style.impl.ts new file mode 100644 index 00000000000000..cfd9e134506c28 --- /dev/null +++ b/packages/remix/src/generators/style/style.impl.ts @@ -0,0 +1,91 @@ +import { + formatFiles, + joinPathFragments, + readProjectConfiguration, + stripIndents, + Tree, +} from '@nx/devkit'; +import { RemixStyleSchema } from './schema'; + +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { dirname, relative } from 'path'; +import { insertImport } from '../../utils/insert-import'; +import { insertStatementAfterImports } from '../../utils/insert-statement-after-imports'; +import { + normalizeRoutePath, + resolveRemixAppDirectory, + resolveRemixRouteFile, +} from '../../utils/remix-route-utils'; + +export default async function (tree: Tree, options: RemixStyleSchema) { + const { project: projectName, artifactName: name } = + await determineArtifactNameAndDirectoryOptions(tree, { + artifactType: 'style', + callingGenerator: '@nx/remix:style', + name: options.path, + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + }); + const project = readProjectConfiguration(tree, projectName); + if (!project) throw new Error(`Project does not exist: ${projectName}`); + + const appDir = await resolveRemixAppDirectory(tree, project.name); + const normalizedRoutePath = `${normalizeRoutePath(options.path) + .replace(/^\//, '') + .replace('.tsx', '')}.css`; + const stylesheetPath = joinPathFragments( + appDir, + 'styles', + normalizedRoutePath + ); + + tree.write( + stylesheetPath, + stripIndents` + :root { + --color-foreground: #fff; + --color-background: #143157; + --color-links: hsl(214, 73%, 69%); + --color-border: #275da8; + --font-body: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + Liberation Mono, Courier New, monospace; + } + ` + ); + + const routeFilePath = options.nameAndDirectoryFormat + ? options.path + : await resolveRemixRouteFile(tree, options.path, options.project, '.tsx'); + + insertImport(tree, routeFilePath, 'LinksFunction', '@remix-run/node', { + typeOnly: true, + }); + + if (project.root === '.') { + insertStatementAfterImports( + tree, + routeFilePath, + ` + import stylesUrl from '~/styles/${normalizedRoutePath}' + + export const links: LinksFunction = () => { + return [{ rel: 'stylesheet', href: stylesUrl }]; + }; + ` + ); + } else { + insertStatementAfterImports( + tree, + routeFilePath, + ` + import stylesUrl from '${relative(dirname(routeFilePath), stylesheetPath)}'; + + export const links: LinksFunction = () => { + return [{ rel: 'stylesheet', href: stylesUrl }]; + }; + ` + ); + } + + await formatFiles(tree); +} diff --git a/packages/remix/src/utils/create-watch-paths.spec.ts b/packages/remix/src/utils/create-watch-paths.spec.ts new file mode 100644 index 00000000000000..fe657316ce0984 --- /dev/null +++ b/packages/remix/src/utils/create-watch-paths.spec.ts @@ -0,0 +1,160 @@ +import { joinPathFragments, workspaceRoot } from '@nx/devkit'; +import { + createWatchPaths, + getRelativeDependencyPaths, +} from './create-watch-paths'; + +describe('createWatchPaths', () => { + it('should list root paths of dependencies relative to project root', async () => { + const testDir = joinPathFragments(workspaceRoot, 'e2e/remix'); + + const paths = await createWatchPaths(testDir); + expect(paths).toEqual(['../../packages', '../../graph', '../../e2e/utils']); + }); +}); + +describe('getRelativeDependencyPaths', () => { + it('should work for standalone projects', () => { + const project = { + type: 'app' as const, + name: 'test', + data: { root: '.', files: [] }, + }; + const result = getRelativeDependencyPaths( + project, + ['lib-1', 'lib-2', 'lib-3'], + { + nodes: { + test: project, + 'lib-1': { + type: 'lib', + name: 'lib-1', + data: { root: 'lib-1' }, + }, + 'lib-2': { + type: 'lib', + name: 'lib-2', + data: { root: 'lib-2' }, + }, + 'lib-3': { + type: 'lib', + name: 'lib-3', + data: { root: 'lib-3' }, + }, + }, + dependencies: {}, + } + ); + + expect(result).toEqual(['lib-1', 'lib-2', 'lib-3']); + }); + + it('should watch the entire libs folder for integrated monorepos', () => { + const project = { + type: 'app' as const, + name: 'test', + data: { root: 'apps/test', files: [] }, + }; + const result = getRelativeDependencyPaths( + project, + ['lib-1', 'lib-2', 'lib-3'], + { + nodes: { + test: project, + 'lib-1': { + type: 'lib', + name: 'lib-1', + data: { root: 'libs/lib-1' }, + }, + 'lib-2': { + type: 'lib', + name: 'lib-2', + data: { root: 'libs/lib-2' }, + }, + 'lib-3': { + type: 'lib', + name: 'lib-3', + data: { root: 'libs/lib-3' }, + }, + }, + dependencies: {}, + } + ); + + expect(result).toEqual(['../../libs']); + }); + + it('should watch the entire packages folder for monorepos if apps is not contained in it', () => { + const project = { + type: 'app' as const, + name: 'test', + data: { root: 'apps/test', files: [] }, + }; + const result = getRelativeDependencyPaths( + project, + ['lib-1', 'lib-2', 'lib-3'], + { + nodes: { + test: project, + 'lib-1': { + type: 'lib', + name: 'lib-1', + data: { root: 'packages/lib-1' }, + }, + 'lib-2': { + type: 'lib', + name: 'lib-2', + data: { root: 'packages/lib-2' }, + }, + 'lib-3': { + type: 'lib', + name: 'lib-3', + data: { root: 'packages/lib-3' }, + }, + }, + dependencies: {}, + } + ); + + expect(result).toEqual(['../../packages']); + }); + + it('should watch individual dependency folder if app is contained in the same base path', () => { + const project = { + type: 'app' as const, + name: 'test', + data: { root: 'packages/test', files: [] }, + }; + const result = getRelativeDependencyPaths( + project, + ['lib-1', 'lib-2', 'lib-3'], + { + nodes: { + test: project, + 'lib-1': { + type: 'lib', + name: 'lib-1', + data: { root: 'packages/lib-1' }, + }, + 'lib-2': { + type: 'lib', + name: 'lib-2', + data: { root: 'packages/lib-2' }, + }, + 'lib-3': { + type: 'lib', + name: 'lib-3', + data: { root: 'packages/lib-3' }, + }, + }, + dependencies: {}, + } + ); + + expect(result).toEqual([ + '../../packages/lib-1', + '../../packages/lib-2', + '../../packages/lib-3', + ]); + }); +}); diff --git a/packages/remix/src/utils/create-watch-paths.ts b/packages/remix/src/utils/create-watch-paths.ts new file mode 100644 index 00000000000000..d262d326cb602e --- /dev/null +++ b/packages/remix/src/utils/create-watch-paths.ts @@ -0,0 +1,59 @@ +import { + createProjectGraphAsync, + joinPathFragments, + offsetFromRoot, + workspaceRoot, + type ProjectGraph, + type ProjectGraphProjectNode, +} from '@nx/devkit'; +import { + createProjectRootMappings, + findProjectForPath, +} from 'nx/src/project-graph/utils/find-project-for-path'; +import { findAllProjectNodeDependencies } from 'nx/src/utils/project-graph-utils'; +import { normalize, relative, sep } from 'path'; + +/** + * Generates an array of paths to watch based on the project dependencies. + * + * @param {string} dirname The absolute path to the Remix project, typically `__dirname`. + */ +export async function createWatchPaths(dirname: string): Promise { + const graph = await createProjectGraphAsync(); + const projectRootMappings = createProjectRootMappings(graph.nodes); + const projectName = findProjectForPath( + relative(workspaceRoot, dirname), + projectRootMappings + ); + const deps = findAllProjectNodeDependencies(projectName, graph); + + return getRelativeDependencyPaths(graph.nodes[projectName], deps, graph); +} + +// Exported for testing +export function getRelativeDependencyPaths( + project: ProjectGraphProjectNode, + deps: string[], + graph: ProjectGraph +): string[] { + if (!project.data?.root) { + throw new Error( + `Project ${project.name} has no root set. Check the project configuration.` + ); + } + + const paths = new Set(); + const offset = offsetFromRoot(project.data.root); + const [baseProjectPath] = project.data.root.split('/'); + + for (const dep of deps) { + const node = graph.nodes[dep]; + if (!node?.data?.root) continue; + const [basePath] = normalize(node.data.root).split(sep); + const watchPath = baseProjectPath !== basePath ? basePath : node.data.root; + const relativeWatchPath = joinPathFragments(offset, watchPath); + paths.add(relativeWatchPath); + } + + return Array.from(paths); +} diff --git a/packages/remix/src/utils/get-default-export-name.spec.ts b/packages/remix/src/utils/get-default-export-name.spec.ts new file mode 100644 index 00000000000000..c7736e6311e572 --- /dev/null +++ b/packages/remix/src/utils/get-default-export-name.spec.ts @@ -0,0 +1,31 @@ +import { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { getDefaultExportName } from './get-default-export-name'; + +describe('getDefaultExportName', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write('.gitignore', `/node_modules/dist`); + }); + + it("should get the default export's name", () => { + tree.write( + 'component.tsx', + `export default function Component() { return (

Hello world!

); };` + ); + + const defaultExportName = getDefaultExportName(tree, 'component.tsx'); + + expect(defaultExportName).toEqual('Component'); + }); + + it("should return 'Unknown' if there is no default export", () => { + tree.write('util.ts', `export function util() { return 'hello world'; };`); + + const defaultExportName = getDefaultExportName(tree, 'util.ts'); + + expect(defaultExportName).toEqual('Unknown'); + }); +}); diff --git a/packages/remix/src/utils/get-default-export-name.ts b/packages/remix/src/utils/get-default-export-name.ts new file mode 100644 index 00000000000000..bda9cab5227937 --- /dev/null +++ b/packages/remix/src/utils/get-default-export-name.ts @@ -0,0 +1,6 @@ +import { Tree } from '@nx/devkit'; +import { getDefaultExport } from './get-default-export'; + +export function getDefaultExportName(tree: Tree, path: string) { + return getDefaultExport(tree, path)?.name.text ?? 'Unknown'; +} diff --git a/packages/remix/src/utils/get-default-export.ts b/packages/remix/src/utils/get-default-export.ts new file mode 100644 index 00000000000000..3adbd7c281e0eb --- /dev/null +++ b/packages/remix/src/utils/get-default-export.ts @@ -0,0 +1,29 @@ +import { Tree } from '@nx/devkit'; +import { + createSourceFile, + isFunctionDeclaration, + ScriptTarget, + SyntaxKind, +} from 'typescript'; + +export function getDefaultExport(tree: Tree, path: string) { + const contents = tree.read(path, 'utf-8'); + + const sourceFile = createSourceFile(path, contents, ScriptTarget.ESNext); + + const functionDeclarations = sourceFile.statements.filter( + isFunctionDeclaration + ); + + return functionDeclarations.find((functionDeclaration) => { + const isDefault = functionDeclaration.modifiers.find( + (mod) => mod.kind === SyntaxKind.DefaultKeyword + ); + + const isExport = functionDeclaration.modifiers.find( + (mod) => mod.kind === SyntaxKind.ExportKeyword + ); + + return isDefault && isExport; + }); +} diff --git a/packages/remix/src/utils/insert-import.spec.ts b/packages/remix/src/utils/insert-import.spec.ts new file mode 100644 index 00000000000000..a0b55ddf40bfd9 --- /dev/null +++ b/packages/remix/src/utils/insert-import.spec.ts @@ -0,0 +1,93 @@ +import { Tree } from '@nx/devkit'; +import { createTree } from '@nx/devkit/testing'; +import { insertImport } from './insert-import'; + +describe('insertImport', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTree(); + }); + + it('should insert a statement after the last import', () => { + tree.write('index.ts', `import { a } from 'a-path';`); + + insertImport(tree, 'index.ts', 'b', 'a-path'); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot( + `"import { a ,b} from 'a-path';"` + ); + }); + + it('should insert a statement after the last import with a trailing comma', () => { + tree.write('index.ts', `import { a, } from 'a-path';`); + + insertImport(tree, 'index.ts', 'b', 'a-path'); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot( + `"import { a, b,} from 'a-path';"` + ); + }); + + it('should insert a statement at the beginning if there are no imports', () => { + tree.write('index.ts', `import { a } from 'a-path';`); + + insertImport(tree, 'index.ts', 'b', 'b-path'); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { a } from 'a-path'; + import { b } from 'b-path';" + `); + }); + + it('should insert a type-only statement after the last import', () => { + tree.write('index.ts', `import type { a } from 'a-path';`); + + insertImport(tree, 'index.ts', 'b', 'a-path', { typeOnly: true }); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot( + `"import type { a ,b} from 'a-path';"` + ); + }); + + it('should insert a type-only statement after the last import with a trailing comma', () => { + tree.write('index.ts', `import type { a, } from 'a-path';`); + + insertImport(tree, 'index.ts', 'b', 'a-path', { typeOnly: true }); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot( + `"import type { a, b,} from 'a-path';"` + ); + }); + + it('should insert a type-only statement at the beginning if there are no imports', () => { + tree.write('index.ts', `import { a } from 'a-path';`); + + insertImport(tree, 'index.ts', 'b', 'b-path', { typeOnly: true }); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { a } from 'a-path'; + import type { b } from 'b-path';" + `); + }); + + it('should not insert a type-only statement into an existing import', () => { + tree.write('index.ts', `import { a } from 'a-path';`); + + insertImport(tree, 'index.ts', 'b', 'a-path', { typeOnly: true }); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { a } from 'a-path'; + import type { b } from 'a-path';" + `); + }); + + it('should not add the same import twice', () => { + tree.write('index.ts', `import { a } from 'a-path';`); + insertImport(tree, 'index.ts', 'a', 'a-path'); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot( + `"import { a } from 'a-path';"` + ); + }); +}); diff --git a/packages/remix/src/utils/insert-import.ts b/packages/remix/src/utils/insert-import.ts new file mode 100644 index 00000000000000..823ecee3f994f2 --- /dev/null +++ b/packages/remix/src/utils/insert-import.ts @@ -0,0 +1,90 @@ +import { applyChangesToString, ChangeType, Tree } from '@nx/devkit'; +import { + createSourceFile, + isImportDeclaration, + isNamedImports, + isStringLiteral, + NamedImports, + ScriptTarget, +} from 'typescript'; +import { insertStatementAfterImports } from './insert-statement-after-imports'; + +export function insertImport( + tree: Tree, + path: string, + name: string, + modulePath: string, + options: { typeOnly: boolean } = { typeOnly: false } +) { + if (!tree.exists(path)) + throw Error( + `Could not insert import ${name} from ${modulePath} in ${path}: path not found` + ); + + const contents = tree.read(path, 'utf-8'); + + const sourceFile = createSourceFile(path, contents, ScriptTarget.ESNext); + + let importStatements = sourceFile.statements.filter(isImportDeclaration); + + if (options.typeOnly) { + importStatements = importStatements.filter( + (node) => node.importClause.isTypeOnly + ); + } else { + importStatements = importStatements.filter( + (node) => !node.importClause.isTypeOnly + ); + } + + const existingImport = importStatements.find( + (statement) => + isStringLiteral(statement.moduleSpecifier) && + statement.moduleSpecifier + .getText(sourceFile) + .replace(/['"`]/g, '') + .trim() === modulePath && + statement.importClause.namedBindings && + isNamedImports(statement.importClause.namedBindings) + ); + + if (!existingImport) { + insertStatementAfterImports( + tree, + path, + options.typeOnly + ? `import type { ${name} } from '${modulePath}';` + : `import { ${name} } from '${modulePath}';` + ); + return; + } + + const namedImports = existingImport.importClause + .namedBindings as NamedImports; + + const alreadyImported = + namedImports.elements.find( + (element) => element.name.escapedText === name + ) !== undefined; + + if (!alreadyImported) { + const index = namedImports.getEnd() - 1; + + let text: string; + if (namedImports.elements.hasTrailingComma) { + text = `${name},`; + } else { + text = `,${name}`; + } + + const newContents = applyChangesToString(contents, [ + { + type: ChangeType.Insert, + index, + text, + }, + ]); + + tree.write(path, newContents); + } +} diff --git a/packages/remix/src/utils/insert-statement-after-imports.spec.ts b/packages/remix/src/utils/insert-statement-after-imports.spec.ts new file mode 100644 index 00000000000000..b515e3fc9b2679 --- /dev/null +++ b/packages/remix/src/utils/insert-statement-after-imports.spec.ts @@ -0,0 +1,33 @@ +import { Tree } from '@nx/devkit'; +import { createTree } from '@nx/devkit/testing'; +import { insertStatementAfterImports } from './insert-statement-after-imports'; + +describe('insertStatement', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTree(); + }); + + it('should insert a statement after the last import', () => { + tree.write('index.ts', `import { a } from 'a';`); + + insertStatementAfterImports(tree, 'index.ts', 'const b = 0;'); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { a } from 'a'; + const b = 0;" + `); + }); + + it('should insert a statement at the beginning if there are no imports', () => { + tree.write('index.ts', `const a = 0;`); + + insertStatementAfterImports(tree, 'index.ts', 'const b = 0;\n'); + + expect(tree.read('index.ts', 'utf-8')).toMatchInlineSnapshot(` + "const b = 0; + const a = 0;" + `); + }); +}); diff --git a/packages/remix/src/utils/insert-statement-after-imports.ts b/packages/remix/src/utils/insert-statement-after-imports.ts new file mode 100644 index 00000000000000..9c6e90d05cb34a --- /dev/null +++ b/packages/remix/src/utils/insert-statement-after-imports.ts @@ -0,0 +1,39 @@ +import { applyChangesToString, ChangeType, Tree } from '@nx/devkit'; +import { + createSourceFile, + isImportDeclaration, + ScriptTarget, +} from 'typescript'; + +/** + * Insert a statement after the last import statement in a file + */ +export function insertStatementAfterImports( + tree: Tree, + path: string, + statement: string +) { + const contents = tree.read(path, 'utf-8'); + + const sourceFile = createSourceFile(path, contents, ScriptTarget.ESNext); + + const importStatements = sourceFile.statements.filter(isImportDeclaration); + const index = + importStatements.length > 0 + ? importStatements[importStatements.length - 1].getEnd() + : 0; + + if (importStatements.length > 0) { + statement = `\n${statement}`; + } + + const newContents = applyChangesToString(contents, [ + { + type: ChangeType.Insert, + index, + text: statement, + }, + ]); + + tree.write(path, newContents); +} diff --git a/packages/remix/src/utils/insert-statement-in-default-function.spec.ts b/packages/remix/src/utils/insert-statement-in-default-function.spec.ts new file mode 100644 index 00000000000000..16ed6c24d97e8e --- /dev/null +++ b/packages/remix/src/utils/insert-statement-in-default-function.spec.ts @@ -0,0 +1,40 @@ +import { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { insertStatementInDefaultFunction } from './insert-statement-in-default-function'; + +describe('insertStatementInDefaultFunction', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write('.gitignore', `/node_modules/dist`); + }); + + it('should insert statement in default function', () => { + tree.write( + 'component.tsx', + `export default function Component() { return (

Hello world!

); };` + ); + + insertStatementInDefaultFunction( + tree, + 'component.tsx', + `const someVar = "whatever";` + ); + + expect(tree.read('component.tsx', 'utf-8')).toMatchInlineSnapshot( + `"export default function Component() {const someVar = "whatever"; return (

Hello world!

); };"` + ); + }); + + it('should throw if there is no default export', () => { + tree.write('util.ts', `export function hello() { return 'helloWorld'; }`); + expect(() => + insertStatementInDefaultFunction( + tree, + 'util.ts', + `const someVar = "whatever";` + ) + ).toThrowErrorMatchingInlineSnapshot(`"No default export found!"`); + }); +}); diff --git a/packages/remix/src/utils/insert-statement-in-default-function.ts b/packages/remix/src/utils/insert-statement-in-default-function.ts new file mode 100644 index 00000000000000..d300f10ae213fd --- /dev/null +++ b/packages/remix/src/utils/insert-statement-in-default-function.ts @@ -0,0 +1,29 @@ +import { applyChangesToString, ChangeType, Tree } from '@nx/devkit'; +import { getDefaultExport } from './get-default-export'; + +export function insertStatementInDefaultFunction( + tree: Tree, + path: string, + statement +) { + const defaultExport = getDefaultExport(tree, path); + + if (!defaultExport) { + throw Error('No default export found!'); + } + + const index = + defaultExport.body.statements.length > 0 + ? defaultExport.body.statements[0].pos + : 0; + + const newContents = applyChangesToString(tree.read(path, 'utf-8'), [ + { + type: ChangeType.Insert, + index, + text: statement, + }, + ]); + + tree.write(path, newContents); +} diff --git a/packages/remix/src/utils/remix-config.ts b/packages/remix/src/utils/remix-config.ts new file mode 100644 index 00000000000000..4d646fee81f04e --- /dev/null +++ b/packages/remix/src/utils/remix-config.ts @@ -0,0 +1,19 @@ +import { joinPathFragments, readProjectConfiguration, Tree } from '@nx/devkit'; +import type { AppConfig } from '@remix-run/dev'; + +export function getRemixConfigPath(tree: Tree, projectName: string) { + const project = readProjectConfiguration(tree, projectName); + if (!project) throw new Error(`Project does not exist: ${projectName}`); + + for (const ext of ['.cjs', '.js']) { + const configPath = joinPathFragments(project.root, `remix.config${ext}`); + if (tree.exists(configPath)) { + return configPath; + } + } +} + +export async function getRemixConfigValues(tree: Tree, projectName: string) { + const remixConfigPath = getRemixConfigPath(tree, projectName); + return eval(tree.read(remixConfigPath, 'utf-8')) as AppConfig; +} diff --git a/packages/remix/src/utils/remix-route-utils.ts b/packages/remix/src/utils/remix-route-utils.ts new file mode 100644 index 00000000000000..36da8f6c2d1ea6 --- /dev/null +++ b/packages/remix/src/utils/remix-route-utils.ts @@ -0,0 +1,95 @@ +import { + joinPathFragments, + names, + readProjectConfiguration, + Tree, +} from '@nx/devkit'; +import { getRemixConfigValues } from './remix-config'; + +/** + * + * @param tree + * @param path to the route which could be fully specified or just "foo/bar" + * @param projectName the name of the project where the route should be added + * @param fileExtension the file extension to add to resolved route file + * @returns file path to the route + */ +export async function resolveRemixRouteFile( + tree: Tree, + path: string, + projectName?: string, + fileExtension?: string +): Promise { + const { name: routePath } = names(path.replace(/^\//, '').replace(/\/$/, '')); + + if (!projectName) { + return appendRouteFileExtension(tree, routePath, fileExtension); + } else { + const project = readProjectConfiguration(tree, projectName); + if (!project) throw new Error(`Project does not exist: ${projectName}`); + const normalizedRoutePath = normalizeRoutePath(routePath); + const fileName = appendRouteFileExtension( + tree, + normalizedRoutePath, + fileExtension + ); + + return joinPathFragments( + await resolveRemixAppDirectory(tree, projectName), + 'routes', + fileName + ); + } +} + +function appendRouteFileExtension( + tree: Tree, + routePath: string, + fileExtension?: string +) { + // if no file extension specified, let's try to find it + if (!fileExtension) { + // see if the path already has it + const extensionMatch = routePath.match(/(\.[^.]+)$/); + + if (extensionMatch) { + fileExtension = extensionMatch[0]; + } else { + // look for either .ts or .tsx to exist in tree + if (tree.exists(`${routePath}.ts`)) { + fileExtension = '.ts'; + } else { + // default to .tsx if nothing else found + fileExtension = '.tsx'; + } + } + } + + return routePath.endsWith(fileExtension) + ? routePath + : `${routePath}${fileExtension}`; +} + +export function normalizeRoutePath(path: string) { + return path.indexOf('/routes/') > -1 + ? path.substring(path.indexOf('/routes/') + 8) + : path; +} + +export function checkRoutePathForErrors(path: string) { + return ( + path.match(/\w\.\.\w/) || // route.$withParams.tsx => route..tsx + path.match(/\w\/\/\w/) || // route/$withParams/index.tsx => route//index.tsx + path.match(/\w\/\.\w/) // route/$withParams.tsx => route/.tsx + ); +} + +export async function resolveRemixAppDirectory( + tree: Tree, + projectName: string +) { + const project = readProjectConfiguration(tree, projectName); + const remixConfig = await getRemixConfigValues(tree, projectName); + + return joinPathFragments(project.root, remixConfig.appDirectory ?? 'app'); +} diff --git a/packages/remix/src/utils/testing-config-utils.ts b/packages/remix/src/utils/testing-config-utils.ts new file mode 100644 index 00000000000000..bdc446ffbb5101 --- /dev/null +++ b/packages/remix/src/utils/testing-config-utils.ts @@ -0,0 +1,128 @@ +import { stripIndents, type Tree } from '@nx/devkit'; +import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; + +let tsModule: typeof import('typescript'); + +export function updateViteTestSetup( + tree: Tree, + pathToViteConfig: string, + pathToTestSetup: string +) { + if (!tsModule) { + tsModule = ensureTypescript(); + } + const { tsquery } = require('@phenomnomnominal/tsquery'); + const fileContents = tree.read(pathToViteConfig, 'utf-8'); + + const ast = tsquery.ast(fileContents); + + const TEST_SETUPFILES_SELECTOR = + 'PropertyAssignment:has(Identifier[name=test]) PropertyAssignment:has(Identifier[name=setupFiles])'; + + const nodes = tsquery(ast, TEST_SETUPFILES_SELECTOR, { + visitAllChildren: true, + }); + + let updatedFileContents = fileContents; + if (nodes.length === 0) { + const TEST_CONFIG_SELECTOR = + 'PropertyAssignment:has(Identifier[name=test]) > ObjectLiteralExpression'; + const testConfigNodes = tsquery(ast, TEST_CONFIG_SELECTOR, { + visitAllChildren: true, + }); + updatedFileContents = stripIndents`${fileContents.slice( + 0, + testConfigNodes[0].getStart() + 1 + )}setupFiles: ['${pathToTestSetup}'],${fileContents.slice( + testConfigNodes[0].getStart() + 1 + )}`; + } else { + const arrayNodes = tsquery(nodes[0], 'ArrayLiteralExpression', { + visitAllChildren: true, + }); + if (arrayNodes.length !== 0) { + updatedFileContents = stripIndents`${fileContents.slice( + 0, + arrayNodes[0].getStart() + 1 + )}'${pathToTestSetup}',${fileContents.slice( + arrayNodes[0].getStart() + 1 + )}`; + } + } + + tree.write(pathToViteConfig, updatedFileContents); +} + +export function updateJestTestSetup( + tree: Tree, + pathToJestConfig: string, + pathToTestSetup: string +) { + if (!tsModule) { + tsModule = ensureTypescript(); + } + const { tsquery } = require('@phenomnomnominal/tsquery'); + const fileContents = tree.read(pathToJestConfig, 'utf-8'); + + const ast = tsquery.ast(fileContents); + + const TEST_SETUPFILES_SELECTOR = + 'PropertyAssignment:has(Identifier[name=setupFilesAfterEnv])'; + const nodes = tsquery(ast, TEST_SETUPFILES_SELECTOR, { + visitAllChildren: true, + }); + + if (nodes.length === 0) { + const CONFIG_SELECTOR = 'ObjectLiteralExpression'; + const nodes = tsquery(ast, CONFIG_SELECTOR, { visitAllChildren: true }); + + const updatedFileContents = stripIndents`${fileContents.slice( + 0, + nodes[0].getStart() + 1 + )}setupFilesAfterEnv: ['${pathToTestSetup}'],${fileContents.slice( + nodes[0].getStart() + 1 + )}`; + tree.write(pathToJestConfig, updatedFileContents); + } else { + const arrayNodes = tsquery(nodes[0], 'ArrayLiteralExpression', { + visitAllChildren: true, + }); + if (arrayNodes.length !== 0) { + const updatedFileContents = stripIndents`${fileContents.slice( + 0, + arrayNodes[0].getStart() + 1 + )}'${pathToTestSetup}',${fileContents.slice( + arrayNodes[0].getStart() + 1 + )}`; + + tree.write(pathToJestConfig, updatedFileContents); + } + } +} + +export function updateViteTestIncludes( + tree: Tree, + pathToViteConfig: string, + includesString: string +) { + if (!tsModule) { + tsModule = ensureTypescript(); + } + const { tsquery } = require('@phenomnomnominal/tsquery'); + const fileContents = tree.read(pathToViteConfig, 'utf-8'); + + const ast = tsquery.ast(fileContents); + + const TEST_INCLUDE_SELECTOR = + 'PropertyAssignment:has(Identifier[name=test]) PropertyAssignment:has(Identifier[name=include])'; + const nodes = tsquery(ast, TEST_INCLUDE_SELECTOR, { visitAllChildren: true }); + + if (nodes.length !== 0) { + const updatedFileContents = stripIndents`${fileContents.slice( + 0, + nodes[0].getStart() + )}include: ["${includesString}"]${fileContents.slice(nodes[0].getEnd())}`; + + tree.write(pathToViteConfig, updatedFileContents); + } +} diff --git a/packages/remix/src/utils/upsert-links-function.spec.ts b/packages/remix/src/utils/upsert-links-function.spec.ts new file mode 100644 index 00000000000000..ccf75dcfac28d2 --- /dev/null +++ b/packages/remix/src/utils/upsert-links-function.spec.ts @@ -0,0 +1,61 @@ +import { stripIndents } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { upsertLinksFunction } from './upsert-links-function'; + +describe('upsertLinksFunctions', () => { + it('should add the imports and the link function when it does not exist', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write(`root.tsx`, ``); + + // ACT + upsertLinksFunction( + tree, + 'root.tsx', + 'styles', + './tailwind.css', + '{ rel: "stylesheet", href: styles }' + ); + + // ASSERT + expect(tree.read('root.tsx', 'utf-8')).toMatchInlineSnapshot(` + "import type { LinksFunction } from '@remix-run/node'; + import styles from "./tailwind.css"; + export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, + ];" + `); + }); + + it('should update an existing links function with the new object', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write( + `root.tsx`, + stripIndents`import type { LinksFunction } from '@remix-run/node'; + + export const links: LinksFunction = () => [ + { rel: "icon", href: "/favicon.png" type: "image/png" } + ` + ); + + // ACT + upsertLinksFunction( + tree, + 'root.tsx', + 'styles', + './tailwind.css', + '{ rel: "stylesheet", href: styles }' + ); + + // ASSERT + expect(tree.read('root.tsx', 'utf-8')).toMatchInlineSnapshot(` + "import type { LinksFunction } from '@remix-run/node'; + import styles from "./tailwind.css"; + + export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, + { rel: "icon", href: "/favicon.png" type: "image/png" }" + `); + }); +}); diff --git a/packages/remix/src/utils/upsert-links-function.ts b/packages/remix/src/utils/upsert-links-function.ts new file mode 100644 index 00000000000000..aac4c79c358fbc --- /dev/null +++ b/packages/remix/src/utils/upsert-links-function.ts @@ -0,0 +1,51 @@ +import { stripIndents, type Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { insertImport } from './insert-import'; +import { insertStatementAfterImports } from './insert-statement-after-imports'; + +export function upsertLinksFunction( + tree: Tree, + filePath: string, + importName: string, + importPath: string, + linkObject: string +) { + insertImport(tree, filePath, 'LinksFunction', '@remix-run/node', { + typeOnly: true, + }); + insertStatementAfterImports( + tree, + filePath, + stripIndents`import ${importName} from "${importPath}";` + ); + + const fileContents = tree.read(filePath, 'utf-8'); + const LINKS_FUNCTION_SELECTOR = + 'VariableDeclaration:has(TypeReference > Identifier[name=LinksFunction])'; + const ast = tsquery.ast(fileContents); + + const linksFunctionNodes = tsquery(ast, LINKS_FUNCTION_SELECTOR, { + visitAllChildren: true, + }); + if (linksFunctionNodes.length === 0) { + insertStatementAfterImports( + tree, + filePath, + stripIndents`export const links: LinksFunction = () => [ + ${linkObject}, +];` + ); + } else { + const linksArrayNodes = tsquery( + linksFunctionNodes[0], + 'ArrayLiteralExpression', + { visitAllChildren: true } + ); + const arrayNode = linksArrayNodes[0]; + const updatedFileContents = `${fileContents.slice( + 0, + arrayNode.getStart() + 1 + )}\n${linkObject},${fileContents.slice(arrayNode.getStart() + 1)}`; + tree.write(filePath, updatedFileContents); + } +} diff --git a/packages/remix/src/utils/versions.ts b/packages/remix/src/utils/versions.ts new file mode 100644 index 00000000000000..43d703c3f7a0d5 --- /dev/null +++ b/packages/remix/src/utils/versions.ts @@ -0,0 +1,29 @@ +import { readJson, Tree } from '@nx/devkit'; + +export const nxVersion = require('../../package.json').version; + +export const remixVersion = '^2.3.0'; +export const isbotVersion = '^3.6.8'; +export const reactVersion = '^18.2.0'; +export const reactDomVersion = '^18.2.0'; +export const typesReactVersion = '^18.2.0'; +export const typesReactDomVersion = '^18.2.0'; +export const eslintVersion = '^8.38.0'; +export const typescriptVersion = '^5.1.6'; +export const tailwindVersion = '^3.3.0'; +export const testingLibraryReactVersion = '^14.1.2'; +export const testingLibraryJestDomVersion = '^6.1.4'; +export const testingLibraryUserEventsVersion = '^14.5.1'; + +export function getRemixVersion(tree: Tree): string { + return getPackageVersion(tree, '@remix-run/dev') ?? remixVersion; +} + +export function getPackageVersion(tree: Tree, packageName: string) { + const packageJsonContents = readJson(tree, 'package.json'); + return ( + packageJsonContents?.['devDependencies']?.[packageName] ?? + packageJsonContents?.['dependencies']?.[packageName] ?? + null + ); +} diff --git a/packages/remix/tsconfig.json b/packages/remix/tsconfig.json new file mode 100644 index 00000000000000..62ebbd946474ce --- /dev/null +++ b/packages/remix/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/remix/tsconfig.lib.json b/packages/remix/tsconfig.lib.json new file mode 100644 index 00000000000000..003684a30d9cdf --- /dev/null +++ b/packages/remix/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "resolveJsonModule": true + }, + "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/remix/tsconfig.spec.json b/packages/remix/tsconfig.spec.json new file mode 100644 index 00000000000000..cc20ac97d13e29 --- /dev/null +++ b/packages/remix/tsconfig.spec.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "resolveJsonModule": true + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts", + "jest.config.ts" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 630eedfe5c70ac..ee6cb61a207d21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -329,6 +329,12 @@ devDependencies: '@reduxjs/toolkit': specifier: 1.9.0 version: 1.9.0(react-redux@8.0.5)(react@18.2.0) + '@remix-run/dev': + specifier: ^2.3.0 + version: 2.3.0(@types/node@18.16.9)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(ts-node@10.9.1)(typescript@5.2.2)(vite@5.0.5) + '@remix-run/node': + specifier: ^2.3.0 + version: 2.3.0(typescript@5.2.2) '@rollup/plugin-babel': specifier: ^5.3.0 version: 5.3.1(@babel/core@7.22.9)(rollup@2.79.0) @@ -2667,6 +2673,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-decorators@7.23.3(@babel/core@7.23.2): + resolution: {integrity: sha512-cf7Niq4/+/juY67E0PbgH0TDhLQ5J7zS8C/Q5FFx+DWyrRa9sUQdTXkjqKu8zGvuqr7vw1muKiukseihU+PJDA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.22.9): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: @@ -2799,16 +2815,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-jsx@7.21.4(@babel/core@7.23.2): - resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.23.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.22.9): resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} engines: {node: '>=6.9.0'} @@ -4724,6 +4730,10 @@ packages: - '@algolia/client-search' dev: false + /@emotion/hash@0.9.1: + resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + dev: true + /@emotion/is-prop-valid@0.8.8: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} requiresBuild: true @@ -4764,6 +4774,15 @@ packages: react: 18.2.0 dev: true + /@esbuild/android-arm64@0.17.6: + resolution: {integrity: sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.18.17: resolution: {integrity: sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==} engines: {node: '>=12'} @@ -4781,6 +4800,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-arm@0.17.6: + resolution: {integrity: sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.18.17: resolution: {integrity: sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==} engines: {node: '>=12'} @@ -4798,6 +4826,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-x64@0.17.6: + resolution: {integrity: sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.18.17: resolution: {integrity: sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==} engines: {node: '>=12'} @@ -4815,6 +4852,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-arm64@0.17.6: + resolution: {integrity: sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.18.17: resolution: {integrity: sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==} engines: {node: '>=12'} @@ -4832,6 +4878,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-x64@0.17.6: + resolution: {integrity: sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.18.17: resolution: {integrity: sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==} engines: {node: '>=12'} @@ -4849,6 +4904,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-arm64@0.17.6: + resolution: {integrity: sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.18.17: resolution: {integrity: sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==} engines: {node: '>=12'} @@ -4866,6 +4930,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-x64@0.17.6: + resolution: {integrity: sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.18.17: resolution: {integrity: sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==} engines: {node: '>=12'} @@ -4883,6 +4956,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm64@0.17.6: + resolution: {integrity: sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.18.17: resolution: {integrity: sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==} engines: {node: '>=12'} @@ -4900,6 +4982,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm@0.17.6: + resolution: {integrity: sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.18.17: resolution: {integrity: sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==} engines: {node: '>=12'} @@ -4917,6 +5008,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ia32@0.17.6: + resolution: {integrity: sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.18.17: resolution: {integrity: sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==} engines: {node: '>=12'} @@ -4934,6 +5034,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-loong64@0.17.6: + resolution: {integrity: sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.18.17: resolution: {integrity: sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==} engines: {node: '>=12'} @@ -4951,6 +5060,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-mips64el@0.17.6: + resolution: {integrity: sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.18.17: resolution: {integrity: sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==} engines: {node: '>=12'} @@ -4968,6 +5086,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ppc64@0.17.6: + resolution: {integrity: sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.18.17: resolution: {integrity: sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==} engines: {node: '>=12'} @@ -4985,6 +5112,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-riscv64@0.17.6: + resolution: {integrity: sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.18.17: resolution: {integrity: sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==} engines: {node: '>=12'} @@ -5002,6 +5138,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-s390x@0.17.6: + resolution: {integrity: sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.18.17: resolution: {integrity: sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==} engines: {node: '>=12'} @@ -5019,6 +5164,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-x64@0.17.6: + resolution: {integrity: sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.18.17: resolution: {integrity: sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==} engines: {node: '>=12'} @@ -5036,6 +5190,15 @@ packages: requiresBuild: true optional: true + /@esbuild/netbsd-x64@0.17.6: + resolution: {integrity: sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.18.17: resolution: {integrity: sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==} engines: {node: '>=12'} @@ -5053,6 +5216,15 @@ packages: requiresBuild: true optional: true + /@esbuild/openbsd-x64@0.17.6: + resolution: {integrity: sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.18.17: resolution: {integrity: sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==} engines: {node: '>=12'} @@ -5070,6 +5242,15 @@ packages: requiresBuild: true optional: true + /@esbuild/sunos-x64@0.17.6: + resolution: {integrity: sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.18.17: resolution: {integrity: sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==} engines: {node: '>=12'} @@ -5087,6 +5268,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-arm64@0.17.6: + resolution: {integrity: sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.18.17: resolution: {integrity: sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==} engines: {node: '>=12'} @@ -5104,6 +5294,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-ia32@0.17.6: + resolution: {integrity: sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.18.17: resolution: {integrity: sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==} engines: {node: '>=12'} @@ -5121,6 +5320,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-x64@0.17.6: + resolution: {integrity: sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.18.17: resolution: {integrity: sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==} engines: {node: '>=12'} @@ -5620,6 +5828,10 @@ packages: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} dev: false + /@jspm/core@2.0.1: + resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==} + dev: true + /@juggle/resize-observer@3.4.0: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} dev: true @@ -5657,12 +5869,36 @@ packages: '@types/markdown-it': 12.2.3 dev: false + /@mdx-js/mdx@2.3.0: + resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} + dependencies: + '@types/estree-jsx': 1.0.3 + '@types/mdx': 2.0.10 + estree-util-build-jsx: 2.2.2 + estree-util-is-identifier-name: 2.1.0 + estree-util-to-js: 1.2.0 + estree-walker: 3.0.3 + hast-util-to-estree: 2.3.3 + markdown-extensions: 1.1.1 + periscopic: 3.1.0 + remark-mdx: 2.3.0 + remark-parse: 10.0.2 + remark-rehype: 10.1.0 + unified: 10.1.2 + unist-util-position-from-estree: 1.1.2 + unist-util-stringify-position: 3.0.3 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + transitivePeerDependencies: + - supports-color + dev: true + /@mdx-js/react@2.3.0(react@18.2.0): resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} peerDependencies: react: '>=16' dependencies: - '@types/mdx': 2.0.4 + '@types/mdx': 2.0.10 '@types/react': 18.2.24 react: 18.2.0 dev: true @@ -6217,6 +6453,22 @@ packages: semver: 7.5.3 dev: true + /@npmcli/git@4.1.0: + resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + '@npmcli/promise-spawn': 6.0.2 + lru-cache: 7.14.0 + npm-pick-manifest: 8.0.2 + proc-log: 3.0.0 + promise-inflight: 1.0.1 + promise-retry: 2.0.1 + semver: 7.5.3 + which: 3.0.1 + transitivePeerDependencies: + - bluebird + dev: true + /@npmcli/git@5.0.3: resolution: {integrity: sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==} engines: {node: ^16.14.0 || >=18.0.0} @@ -6256,6 +6508,28 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /@npmcli/package-json@4.0.1: + resolution: {integrity: sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + '@npmcli/git': 4.1.0 + glob: 10.2.2 + hosted-git-info: 6.1.1 + json-parse-even-better-errors: 3.0.0 + normalize-package-data: 5.0.0 + proc-log: 3.0.0 + semver: 7.5.3 + transitivePeerDependencies: + - bluebird + dev: true + + /@npmcli/promise-spawn@6.0.2: + resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + which: 3.0.1 + dev: true + /@npmcli/promise-spawn@7.0.0: resolution: {integrity: sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==} engines: {node: ^16.14.0 || >=18.0.0} @@ -8448,9 +8722,186 @@ packages: reselect: 4.1.7 dev: true - /@remix-run/router@1.6.2: - resolution: {integrity: sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA==} - engines: {node: '>=14'} + /@remix-run/dev@2.3.0(@types/node@18.16.9)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(ts-node@10.9.1)(typescript@5.2.2)(vite@5.0.5): + resolution: {integrity: sha512-Eno0XHyIKo5GyzN4OAwNkgkyl4H1mLWbqeVUA8T5HmVDj+8qJLIcYeayS2BmA1KYAHJBiy5ufAGi2MpaXMjKww==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + '@remix-run/serve': ^2.3.0 + typescript: ^5.1.0 + vite: ^4.4.9 || ^5.0.0 + peerDependenciesMeta: + '@remix-run/serve': + optional: true + typescript: + optional: true + vite: + optional: true + dependencies: + '@babel/core': 7.23.2 + '@babel/generator': 7.23.0 + '@babel/parser': 7.23.0 + '@babel/plugin-syntax-decorators': 7.23.3(@babel/core@7.23.2) + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.2) + '@babel/preset-typescript': 7.22.5(@babel/core@7.23.2) + '@babel/traverse': 7.23.2 + '@babel/types': 7.23.0 + '@mdx-js/mdx': 2.3.0 + '@npmcli/package-json': 4.0.1 + '@remix-run/node': 2.3.0(typescript@5.2.2) + '@remix-run/router': 1.12.0-pre.0 + '@remix-run/server-runtime': 2.3.0(typescript@5.2.2) + '@types/mdx': 2.0.10 + '@vanilla-extract/integration': 6.2.4(@types/node@18.16.9)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0) + arg: 5.0.2 + cacache: 17.1.4 + chalk: 4.1.2 + chokidar: 3.5.3 + cross-spawn: 7.0.3 + dotenv: 16.3.1 + es-module-lexer: 1.4.1 + esbuild: 0.17.6 + esbuild-plugins-node-modules-polyfill: 1.6.1(esbuild@0.17.6) + execa: 5.1.1 + exit-hook: 2.2.1 + express: 4.18.2 + fs-extra: 10.1.0 + get-port: 5.1.1 + gunzip-maybe: 1.4.2 + jsesc: 3.0.2 + json5: 2.2.3 + lodash: 4.17.21 + lodash.debounce: 4.0.8 + minimatch: 9.0.3 + node-fetch: 2.6.12 + ora: 5.4.1 + parse-multipart-data: 1.5.0 + picocolors: 1.0.0 + picomatch: 2.3.1 + pidtree: 0.6.0 + postcss: 8.4.19 + postcss-discard-duplicates: 5.1.0(postcss@8.4.19) + postcss-load-config: 4.0.2(postcss@8.4.19)(ts-node@10.9.1) + postcss-modules: 6.0.0(postcss@8.4.19) + prettier: 2.7.1 + pretty-ms: 7.0.1 + react-refresh: 0.14.0 + remark-frontmatter: 4.0.1 + remark-mdx-frontmatter: 1.1.1 + semver: 7.5.3 + set-cookie-parser: 2.6.0 + tar-fs: 2.1.1 + tsconfig-paths: 4.1.2 + typescript: 5.2.2 + undici: 5.27.2 + vite: 5.0.5(@types/node@18.16.9)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0) + ws: 7.5.9 + transitivePeerDependencies: + - '@types/node' + - bluebird + - bufferutil + - encoding + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + - ts-node + - utf-8-validate + dev: true + + /@remix-run/node@2.3.0(typescript@5.2.2): + resolution: {integrity: sha512-WQybWc1EWPLMD/btDtchVrhoLvz/ek6MB0gr2cV2N3Sxgn1VaJmpsN3+sUA5lK8vR2S/kOmGun2Ut3tKi8TKHg==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@remix-run/server-runtime': 2.3.0(typescript@5.2.2) + '@remix-run/web-fetch': 4.4.2 + '@remix-run/web-file': 3.1.0 + '@remix-run/web-stream': 1.1.0 + '@web3-storage/multipart-parser': 1.0.0 + cookie-signature: 1.2.1 + source-map-support: 0.5.21 + stream-slice: 0.1.2 + typescript: 5.2.2 + dev: true + + /@remix-run/router@1.12.0: + resolution: {integrity: sha512-2hXv036Bux90e1GXTWSMfNzfDDK8LA8JYEWfyHxzvwdp6GyoWEovKc9cotb3KCKmkdwsIBuFGX7ScTWyiHv7Eg==} + engines: {node: '>=14.0.0'} + dev: true + + /@remix-run/router@1.12.0-pre.0: + resolution: {integrity: sha512-+bBn9KqD2AC0pttSGydVFOZSsT0NqQ1+rGFwMTx9dRANk6oGxrPbKTDxLLikocscGzSL5przvcK4Uxfq8yU7BQ==} + engines: {node: '>=14.0.0'} + dev: true + + /@remix-run/router@1.6.2: + resolution: {integrity: sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA==} + engines: {node: '>=14'} + dev: true + + /@remix-run/server-runtime@2.3.0(typescript@5.2.2): + resolution: {integrity: sha512-9BiRK7VPm5nt/aOlRmeROXWA8HKgqjvQy+f9NNpqvf3jj62EUl0h4eUdyqRj6nNh44I+0XUBG7ZQ2xXTrGJATw==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@remix-run/router': 1.12.0 + '@types/cookie': 0.5.4 + '@web3-storage/multipart-parser': 1.0.0 + cookie: 0.5.0 + set-cookie-parser: 2.6.0 + source-map: 0.7.3 + typescript: 5.2.2 + dev: true + + /@remix-run/web-blob@3.1.0: + resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} + dependencies: + '@remix-run/web-stream': 1.1.0 + web-encoding: 1.1.5 + dev: true + + /@remix-run/web-fetch@4.4.2: + resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==} + engines: {node: ^10.17 || >=12.3} + dependencies: + '@remix-run/web-blob': 3.1.0 + '@remix-run/web-file': 3.1.0 + '@remix-run/web-form-data': 3.1.0 + '@remix-run/web-stream': 1.1.0 + '@web3-storage/multipart-parser': 1.0.0 + abort-controller: 3.0.0 + data-uri-to-buffer: 3.0.1 + mrmime: 1.0.1 + dev: true + + /@remix-run/web-file@3.1.0: + resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==} + dependencies: + '@remix-run/web-blob': 3.1.0 + dev: true + + /@remix-run/web-form-data@3.1.0: + resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==} + dependencies: + web-encoding: 1.1.5 + dev: true + + /@remix-run/web-stream@1.1.0: + resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} + dependencies: + web-streams-polyfill: 3.2.1 dev: true /@rollup/plugin-babel@5.3.1(@babel/core@7.22.9)(rollup@2.79.0): @@ -10373,6 +10824,12 @@ packages: minimatch: 9.0.3 dev: true + /@types/acorn@4.0.6: + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + dependencies: + '@types/estree': 1.0.1 + dev: true + /@types/aria-query@4.2.2: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} dev: true @@ -10473,6 +10930,10 @@ packages: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: true + /@types/cookie@0.5.4: + resolution: {integrity: sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==} + dev: true + /@types/cors@2.8.13: resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} dependencies: @@ -10519,6 +10980,12 @@ packages: '@types/json-schema': 7.0.12 dev: true + /@types/estree-jsx@1.0.3: + resolution: {integrity: sha512-pvQ+TKeRHeiUGRhvYwRrQ/ISnohKkSJR14fT2yqyZ4e9K5vqc7hrtY2Y1Dw0ZwAzQ6DQsxsaCUuSIIi8v0Cq6w==} + dependencies: + '@types/estree': 1.0.1 + dev: true + /@types/estree@0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} dev: true @@ -10754,8 +11221,8 @@ packages: dev: false optional: true - /@types/mdx@2.0.4: - resolution: {integrity: sha512-qCYrNdpKwN6YO6FVnx+ulfqifKlE3lQGsNhvDaW9Oxzyob/cRLBJWow8GHBBD4NxQ7BVvtsATgLsX0vZAWmtrg==} + /@types/mdx@2.0.10: + resolution: {integrity: sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg==} dev: true /@types/mime@3.0.1: @@ -11269,6 +11736,61 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@vanilla-extract/babel-plugin-debug-ids@1.0.3: + resolution: {integrity: sha512-vm4jYu1xhSa6ofQ9AhIpR3DkAp4c+eoR1Rpm8/TQI4DmWbmGbOjYRcqV0aWsfaIlNhN4kFuxFMKBNN9oG6iRzA==} + dependencies: + '@babel/core': 7.23.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@vanilla-extract/css@1.14.0: + resolution: {integrity: sha512-rYfm7JciWZ8PFzBM/HDiE2GLnKI3xJ6/vdmVJ5BSgcCZ5CxRlM9Cjqclni9lGzF3eMOijnUhCd/KV8TOzyzbMA==} + dependencies: + '@emotion/hash': 0.9.1 + '@vanilla-extract/private': 1.0.3 + chalk: 4.1.2 + css-what: 6.1.0 + cssesc: 3.0.0 + csstype: 3.1.1 + deep-object-diff: 1.1.9 + deepmerge: 4.3.1 + media-query-parser: 2.0.2 + modern-ahocorasick: 1.0.1 + outdent: 0.8.0 + dev: true + + /@vanilla-extract/integration@6.2.4(@types/node@18.16.9)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0): + resolution: {integrity: sha512-+AfymNMVq9sEUe0OJpdCokmPZg4Zi6CqKaW/PnUOfDwEn53ighHOMOBl5hAgxYR8Kiz9NG43Bn00mkjWlFi+ng==} + dependencies: + '@babel/core': 7.23.2 + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.23.2) + '@vanilla-extract/babel-plugin-debug-ids': 1.0.3 + '@vanilla-extract/css': 1.14.0 + esbuild: 0.17.6 + eval: 0.1.8 + find-up: 5.0.0 + javascript-stringify: 2.1.0 + lodash: 4.17.21 + mlly: 1.4.2 + outdent: 0.8.0 + vite: 4.5.0(@types/node@18.16.9)(less@4.2.0)(sass@1.69.5)(stylus@0.59.0)(terser@5.24.0) + vite-node: 0.28.5(@types/node@18.16.9)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /@vanilla-extract/private@1.0.3: + resolution: {integrity: sha512-17kVyLq3ePTKOkveHxXuIJZtGYs+cSoev7BlP+Lf4916qfDhk/HBjvlYDe8egrea7LNPHKwSZJK/bzZC+Q6AwQ==} + dev: true + /@verdaccio/commons-api@10.2.0: resolution: {integrity: sha512-F/YZANu4DmpcEV0jronzI7v2fGVWkQ5Mwi+bVmV+ACJ+EzR0c9Jbhtbe5QyLUuzR97t8R5E/Xe53O0cc2LukdQ==} engines: {node: '>=8'} @@ -11452,6 +11974,10 @@ packages: resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} dev: true + /@web3-storage/multipart-parser@1.0.0: + resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + dev: true + /@webassemblyjs/ast@1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} dependencies: @@ -11743,6 +12269,12 @@ packages: dependencies: argparse: 2.0.1 + /@zxing/text-encoding@0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: true + optional: true + /JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -12308,6 +12840,11 @@ packages: engines: {node: '>=8'} dev: true + /astring@1.8.6: + resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} + hasBin: true + dev: true + /async-each-series@0.1.1: resolution: {integrity: sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==} engines: {node: '>=0.8.0'} @@ -13073,6 +13610,12 @@ packages: - utf-8-validate dev: true + /browserify-zlib@0.1.4: + resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} + dependencies: + pako: 0.2.9 + dev: true + /browserslist@4.21.4: resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -13252,7 +13795,6 @@ packages: /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - dev: false /cacache@16.1.3: resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} @@ -13280,6 +13822,24 @@ packages: - bluebird dev: true + /cacache@17.1.4: + resolution: {integrity: sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + '@npmcli/fs': 3.1.0 + fs-minipass: 3.0.0 + glob: 10.2.2 + lru-cache: 7.14.0 + minipass: 7.0.3 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 4.0.0 + ssri: 10.0.4 + tar: 6.2.0 + unique-filename: 3.0.0 + dev: true + /cacache@18.0.0: resolution: {integrity: sha512-I7mVOPl3PUCeRub1U8YoGz2Lqv9WOBpobZ8RyWFXmReuILz+3OAyTa5oH3QPdtKZD7N0Yk00aLfzn0qvp8dZ1w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -13422,6 +13982,10 @@ packages: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} dev: true + /ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + dev: true + /chai@4.3.10: resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} @@ -13483,10 +14047,18 @@ packages: engines: {node: '>=10'} dev: true + /character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + dev: true + /character-entities-legacy@1.1.4: resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} dev: false + /character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + dev: true + /character-entities@1.2.4: resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} dev: false @@ -13499,6 +14071,10 @@ packages: resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} dev: false + /character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + dev: true + /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true @@ -14083,6 +14659,11 @@ packages: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: true + /cookie-signature@1.2.1: + resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} + engines: {node: '>=6.6.0'} + dev: true + /cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} @@ -14753,6 +15334,11 @@ packages: assert-plus: 1.0.0 dev: true + /data-uri-to-buffer@3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + dev: true + /data-urls@2.0.0: resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} engines: {node: '>=10'} @@ -14901,6 +15487,10 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deep-object-diff@1.1.9: + resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} + dev: true + /deepmerge@4.2.2: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} @@ -15225,6 +15815,15 @@ packages: /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + /duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.1 + dev: true + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -15476,6 +16075,10 @@ packages: resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} dev: true + /es-module-lexer@1.4.1: + resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + dev: true + /es-shim-unscopables@1.0.0: resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} dependencies: @@ -15529,6 +16132,18 @@ packages: resolution: {integrity: sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==} dev: true + /esbuild-plugins-node-modules-polyfill@1.6.1(esbuild@0.17.6): + resolution: {integrity: sha512-6sAwI24PV8W0zxeO+i4BS5zoQypS3SzEGwIdxpzpy65riRuK8apMw8PN0aKVLCTnLr0FgNIxUMRd9BsreBrtog==} + engines: {node: '>=14.0.0'} + peerDependencies: + esbuild: ^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0 || ^0.18.0 || ^0.19.0 + dependencies: + '@jspm/core': 2.0.1 + esbuild: 0.17.6 + local-pkg: 0.4.3 + resolve.exports: 2.0.2 + dev: true + /esbuild-register@3.5.0(esbuild@0.18.17): resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==} peerDependencies: @@ -15546,6 +16161,36 @@ packages: hasBin: true dev: true + /esbuild@0.17.6: + resolution: {integrity: sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.6 + '@esbuild/android-arm64': 0.17.6 + '@esbuild/android-x64': 0.17.6 + '@esbuild/darwin-arm64': 0.17.6 + '@esbuild/darwin-x64': 0.17.6 + '@esbuild/freebsd-arm64': 0.17.6 + '@esbuild/freebsd-x64': 0.17.6 + '@esbuild/linux-arm': 0.17.6 + '@esbuild/linux-arm64': 0.17.6 + '@esbuild/linux-ia32': 0.17.6 + '@esbuild/linux-loong64': 0.17.6 + '@esbuild/linux-mips64el': 0.17.6 + '@esbuild/linux-ppc64': 0.17.6 + '@esbuild/linux-riscv64': 0.17.6 + '@esbuild/linux-s390x': 0.17.6 + '@esbuild/linux-x64': 0.17.6 + '@esbuild/netbsd-x64': 0.17.6 + '@esbuild/openbsd-x64': 0.17.6 + '@esbuild/sunos-x64': 0.17.6 + '@esbuild/win32-arm64': 0.17.6 + '@esbuild/win32-ia32': 0.17.6 + '@esbuild/win32-x64': 0.17.6 + dev: true + /esbuild@0.18.17: resolution: {integrity: sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==} engines: {node: '>=12'} @@ -16069,6 +16714,50 @@ packages: - supports-color dev: true + /estree-util-attach-comments@2.1.1: + resolution: {integrity: sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==} + dependencies: + '@types/estree': 1.0.1 + dev: true + + /estree-util-build-jsx@2.2.2: + resolution: {integrity: sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==} + dependencies: + '@types/estree-jsx': 1.0.3 + estree-util-is-identifier-name: 2.1.0 + estree-walker: 3.0.3 + dev: true + + /estree-util-is-identifier-name@1.1.0: + resolution: {integrity: sha512-OVJZ3fGGt9By77Ix9NhaRbzfbDV/2rx9EP7YIDJTmsZSEc5kYn2vWcNccYyahJL2uAQZK2a5Or2i0wtIKTPoRQ==} + dev: true + + /estree-util-is-identifier-name@2.1.0: + resolution: {integrity: sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==} + dev: true + + /estree-util-to-js@1.2.0: + resolution: {integrity: sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==} + dependencies: + '@types/estree-jsx': 1.0.3 + astring: 1.8.6 + source-map: 0.7.3 + dev: true + + /estree-util-value-to-estree@1.3.0: + resolution: {integrity: sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==} + engines: {node: '>=12.0.0'} + dependencies: + is-plain-obj: 3.0.0 + dev: true + + /estree-util-visit@1.2.1: + resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} + dependencies: + '@types/estree-jsx': 1.0.3 + '@types/unist': 2.0.6 + dev: true + /estree-walker@0.6.1: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} dev: true @@ -16096,6 +16785,14 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + /eval@0.1.8: + resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} + engines: {node: '>= 0.8'} + dependencies: + '@types/node': 18.16.9 + require-like: 0.1.2 + dev: true + /event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} dependencies: @@ -16179,6 +16876,11 @@ packages: pify: 2.3.0 dev: true + /exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + dev: true + /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -16419,6 +17121,12 @@ packages: format: 0.2.2 dev: false + /fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + dependencies: + format: 0.2.2 + dev: true + /faye-websocket@0.11.4: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} @@ -16779,7 +17487,6 @@ packages: /format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} - dev: false /formdata-node@4.4.1: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} @@ -17016,6 +17723,11 @@ packages: yargs: 16.2.0 dev: true + /get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + dev: true + /get-stdin@8.0.0: resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} engines: {node: '>=10'} @@ -17340,6 +18052,18 @@ packages: lodash: 4.17.21 dev: true + /gunzip-maybe@1.4.2: + resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} + hasBin: true + dependencies: + browserify-zlib: 0.1.4 + is-deflate: 1.0.0 + is-gzip: 1.0.0 + peek-stream: 1.1.3 + pumpify: 1.5.1 + through2: 2.0.5 + dev: true + /handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} dev: true @@ -17428,6 +18152,28 @@ packages: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} dev: false + /hast-util-to-estree@2.3.3: + resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==} + dependencies: + '@types/estree': 1.0.1 + '@types/estree-jsx': 1.0.3 + '@types/hast': 2.3.4 + '@types/unist': 2.0.6 + comma-separated-tokens: 2.0.3 + estree-util-attach-comments: 2.1.1 + estree-util-is-identifier-name: 2.1.0 + hast-util-whitespace: 2.0.1 + mdast-util-mdx-expression: 1.3.2 + mdast-util-mdxjs-esm: 1.3.1 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.2 + unist-util-position: 4.0.4 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + dev: true + /hast-util-whitespace@2.0.1: resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} dev: true @@ -17507,6 +18253,13 @@ packages: lru-cache: 6.0.0 dev: true + /hosted-git-info@6.1.1: + resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + lru-cache: 7.14.0 + dev: true + /hosted-git-info@7.0.0: resolution: {integrity: sha512-ICclEpTLhHj+zCuSb2/usoNXSVkxUSIopre+b1w8NDY9Dntp9LO4vLdHYI336TH8sAqwrRgnSfdkBG2/YpisHA==} engines: {node: ^16.14.0 || >=18.0.0} @@ -18105,6 +18858,10 @@ packages: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} dev: false + /is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + dev: true + /is-alphanumerical@1.0.4: resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} dependencies: @@ -18112,6 +18869,13 @@ packages: is-decimal: 1.0.4 dev: false + /is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + dev: true + /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -18200,6 +18964,14 @@ packages: resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} dev: false + /is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + dev: true + + /is-deflate@1.0.0: + resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} + dev: true + /is-directory@0.3.1: resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} engines: {node: '>=0.10.0'} @@ -18237,10 +19009,19 @@ packages: dependencies: is-extglob: 2.1.1 + /is-gzip@1.0.0: + resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} + engines: {node: '>=0.10.0'} + dev: true + /is-hexadecimal@1.0.4: resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} dev: false + /is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + dev: true + /is-installed-globally@0.4.0: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} @@ -18565,6 +19346,10 @@ packages: colors: 1.1.2 dev: true + /javascript-stringify@2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + dev: true + /jest-changed-files@29.4.3: resolution: {integrity: sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -18910,7 +19695,7 @@ packages: dependencies: '@babel/core': 7.23.2 '@babel/generator': 7.23.0 - '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.23.2) + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.2) '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.23.2) '@babel/traverse': 7.23.2 '@babel/types': 7.23.0 @@ -19146,6 +19931,12 @@ packages: engines: {node: '>=4'} hasBin: true + /jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + dev: true + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true @@ -19560,6 +20351,11 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} + dev: true + /limiter@1.1.5: resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} dev: true @@ -19638,7 +20434,6 @@ packages: /local-pkg@0.4.3: resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} engines: {node: '>=14'} - dev: false /local-pkg@0.5.0: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} @@ -20003,6 +20798,11 @@ packages: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: true + /markdown-extensions@1.1.1: + resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} + engines: {node: '>=0.10.0'} + dev: true + /markdown-factory@0.0.6: resolution: {integrity: sha512-epJKNY4rlcMIJ+czEkPgstlk+9cKmHUkhRxemCPf+38vKbehBoiH9gmsxXkgzRYQx98hpE9l/zVkg2WI+IbT3Q==} dev: true @@ -20056,21 +20856,84 @@ packages: unist-util-visit: 4.1.2 dev: true - /mdast-util-from-markdown@1.3.1: - resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + /mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + dependencies: + '@types/mdast': 3.0.12 + '@types/unist': 2.0.6 + decode-named-character-reference: 1.0.2 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: true + + /mdast-util-frontmatter@1.0.1: + resolution: {integrity: sha512-JjA2OjxRqAa8wEG8hloD0uTU0kdn8kbtOWpPP94NBkfAlbxn4S8gCGf/9DwFtEeGPXrDcNXdiDjVaRdUFqYokw==} + dependencies: + '@types/mdast': 3.0.12 + mdast-util-to-markdown: 1.5.0 + micromark-extension-frontmatter: 1.1.1 + dev: true + + /mdast-util-mdx-expression@1.3.2: + resolution: {integrity: sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==} + dependencies: + '@types/estree-jsx': 1.0.3 + '@types/hast': 2.3.4 + '@types/mdast': 3.0.12 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /mdast-util-mdx-jsx@2.1.4: + resolution: {integrity: sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==} + dependencies: + '@types/estree-jsx': 1.0.3 + '@types/hast': 2.3.4 + '@types/mdast': 3.0.12 + '@types/unist': 2.0.6 + ccount: 2.0.1 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + parse-entities: 4.0.1 + stringify-entities: 4.0.3 + unist-util-remove-position: 4.0.2 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + transitivePeerDependencies: + - supports-color + dev: true + + /mdast-util-mdx@2.0.1: + resolution: {integrity: sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==} + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-mdx-expression: 1.3.2 + mdast-util-mdx-jsx: 2.1.4 + mdast-util-mdxjs-esm: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /mdast-util-mdxjs-esm@1.3.1: + resolution: {integrity: sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==} dependencies: + '@types/estree-jsx': 1.0.3 + '@types/hast': 2.3.4 '@types/mdast': 3.0.12 - '@types/unist': 2.0.6 - decode-named-character-reference: 1.0.2 - mdast-util-to-string: 3.2.0 - micromark: 3.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-decode-string: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-stringify-position: 3.0.3 - uvu: 0.5.6 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 transitivePeerDependencies: - supports-color dev: true @@ -20130,6 +20993,12 @@ packages: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} dev: true + /media-query-parser@2.0.2: + resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==} + dependencies: + '@babel/runtime': 7.23.2 + dev: true + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -20438,13 +21307,13 @@ packages: engines: {node: '>=16'} hasBin: true dependencies: - '@babel/code-frame': 7.22.5 + '@babel/code-frame': 7.22.13 '@babel/core': 7.23.2 - '@babel/generator': 7.22.9 - '@babel/parser': 7.22.7 + '@babel/generator': 7.23.0 + '@babel/parser': 7.23.0 '@babel/template': 7.22.5 - '@babel/traverse': 7.22.8 - '@babel/types': 7.22.5 + '@babel/traverse': 7.23.2 + '@babel/types': 7.23.0 accepts: 1.3.8 async: 3.2.4 chalk: 4.1.2 @@ -20514,6 +21383,76 @@ packages: uvu: 0.5.6 dev: true + /micromark-extension-frontmatter@1.1.1: + resolution: {integrity: sha512-m2UH9a7n3W8VAH9JO9y01APpPKmNNNs71P0RbknEmYSaZU5Ghogv38BYO94AI5Xw6OYfxZRdHZZ2nYjs/Z+SZQ==} + dependencies: + fault: 2.0.1 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: true + + /micromark-extension-mdx-expression@1.0.8: + resolution: {integrity: sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==} + dependencies: + '@types/estree': 1.0.1 + micromark-factory-mdx-expression: 1.0.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: true + + /micromark-extension-mdx-jsx@1.0.5: + resolution: {integrity: sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==} + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.1 + estree-util-is-identifier-name: 2.1.0 + micromark-factory-mdx-expression: 1.0.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + vfile-message: 3.1.4 + dev: true + + /micromark-extension-mdx-md@1.0.1: + resolution: {integrity: sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==} + dependencies: + micromark-util-types: 1.1.0 + dev: true + + /micromark-extension-mdxjs-esm@1.0.5: + resolution: {integrity: sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==} + dependencies: + '@types/estree': 1.0.1 + micromark-core-commonmark: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-position-from-estree: 1.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + dev: true + + /micromark-extension-mdxjs@1.0.1: + resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + micromark-extension-mdx-expression: 1.0.8 + micromark-extension-mdx-jsx: 1.0.5 + micromark-extension-mdx-md: 1.0.1 + micromark-extension-mdxjs-esm: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + dev: true + /micromark-factory-destination@1.1.0: resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} dependencies: @@ -20531,6 +21470,19 @@ packages: uvu: 0.5.6 dev: true + /micromark-factory-mdx-expression@1.0.9: + resolution: {integrity: sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==} + dependencies: + '@types/estree': 1.0.1 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-position-from-estree: 1.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + dev: true + /micromark-factory-space@1.1.0: resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} dependencies: @@ -20603,6 +21555,19 @@ packages: resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} dev: true + /micromark-util-events-to-acorn@1.2.3: + resolution: {integrity: sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==} + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.1 + '@types/unist': 2.0.6 + estree-util-visit: 1.2.1 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + vfile-message: 3.1.4 + dev: true + /micromark-util-html-tag-name@1.2.0: resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} dev: true @@ -20929,6 +21894,10 @@ packages: pkg-types: 1.0.3 ufo: 1.3.1 + /modern-ahocorasick@1.0.1: + resolution: {integrity: sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA==} + dev: true + /modify-values@1.0.1: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} engines: {node: '>=0.10.0'} @@ -21335,6 +22304,16 @@ packages: validate-npm-package-license: 3.0.4 dev: true + /normalize-package-data@5.0.0: + resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + hosted-git-info: 6.1.1 + is-core-module: 2.13.0 + semver: 7.5.3 + validate-npm-package-license: 3.0.4 + dev: true + /normalize-package-data@6.0.0: resolution: {integrity: sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==} engines: {node: ^16.14.0 || >=18.0.0} @@ -21382,6 +22361,16 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /npm-package-arg@10.1.0: + resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + hosted-git-info: 6.1.1 + proc-log: 3.0.0 + semver: 7.5.3 + validate-npm-package-name: 5.0.0 + dev: true + /npm-package-arg@11.0.1: resolution: {integrity: sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==} engines: {node: ^16.14.0 || >=18.0.0} @@ -21399,6 +22388,16 @@ packages: ignore-walk: 6.0.3 dev: true + /npm-pick-manifest@8.0.2: + resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + npm-install-checks: 6.0.0 + npm-normalize-package-bin: 3.0.0 + npm-package-arg: 10.1.0 + semver: 7.5.3 + dev: true + /npm-pick-manifest@9.0.0: resolution: {integrity: sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==} engines: {node: ^16.14.0 || >=18.0.0} @@ -21903,6 +22902,10 @@ packages: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} dev: true + /outdent@0.8.0: + resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} + dev: true + /p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -22036,6 +23039,10 @@ packages: - supports-color dev: true + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: true + /pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} dev: true @@ -22065,6 +23072,19 @@ packages: is-hexadecimal: 1.0.4 dev: false + /parse-entities@4.0.1: + resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + dependencies: + '@types/unist': 2.0.6 + character-entities: 2.0.2 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + dev: true + /parse-json@4.0.0: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} @@ -22095,6 +23115,10 @@ packages: engines: {node: '>=6'} dev: true + /parse-multipart-data@1.5.0: + resolution: {integrity: sha512-ck5zaMF0ydjGfejNMnlo5YU2oJ+pT+80Jb1y4ybanT27j+zbVP/jkYmCrUGsEln0Ox/hZmuvgy8Ra7AxbXP2Mw==} + dev: true + /parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -22219,6 +23243,14 @@ packages: engines: {node: '>=14.16'} dev: true + /peek-stream@1.1.3: + resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + dependencies: + buffer-from: 1.1.2 + duplexify: 3.7.1 + through2: 2.0.5 + dev: true + /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true @@ -22251,6 +23283,12 @@ packages: engines: {node: '>=10'} dev: true + /pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + dev: true + /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -22737,6 +23775,24 @@ packages: ts-node: 10.9.1(@swc/core@1.3.86)(@types/node@18.16.9)(typescript@5.2.2) yaml: 1.10.2 + /postcss-load-config@4.0.2(postcss@8.4.19)(ts-node@10.9.1): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.0.0 + postcss: 8.4.19 + ts-node: 10.9.1(@swc/core@1.3.86)(@types/node@18.16.9)(typescript@5.2.2) + yaml: 2.3.4 + dev: true + /postcss-loader@6.2.1(postcss@8.4.19)(webpack@5.88.0): resolution: {integrity: sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==} engines: {node: '>= 12.13.0'} @@ -23017,6 +24073,22 @@ packages: string-hash: 1.1.3 dev: true + /postcss-modules@6.0.0(postcss@8.4.19): + resolution: {integrity: sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ==} + peerDependencies: + postcss: ^8.0.0 + dependencies: + generic-names: 4.0.0 + icss-utils: 5.1.0(postcss@8.4.19) + lodash.camelcase: 4.3.0 + postcss: 8.4.19 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.19) + postcss-modules-local-by-default: 4.0.3(postcss@8.4.19) + postcss-modules-scope: 3.0.0(postcss@8.4.19) + postcss-modules-values: 4.0.0(postcss@8.4.19) + string-hash: 1.1.3 + dev: true + /postcss-nested@6.0.0(postcss@8.4.19): resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} engines: {node: '>=12.0'} @@ -23725,6 +24797,13 @@ packages: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: true + /pump@2.0.1: + resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -23732,6 +24811,14 @@ packages: once: 1.4.0 dev: true + /pumpify@1.5.1: + resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} + dependencies: + duplexify: 3.7.1 + inherits: 2.0.4 + pump: 2.0.1 + dev: true + /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: true @@ -24015,6 +25102,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /react-refresh@0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + /react-refresh@0.4.3: resolution: {integrity: sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==} engines: {node: '>=0.10.0'} @@ -24380,6 +25472,34 @@ packages: unist-util-visit: 2.0.3 dev: true + /remark-frontmatter@4.0.1: + resolution: {integrity: sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==} + dependencies: + '@types/mdast': 3.0.12 + mdast-util-frontmatter: 1.0.1 + micromark-extension-frontmatter: 1.1.1 + unified: 10.1.2 + dev: true + + /remark-mdx-frontmatter@1.1.1: + resolution: {integrity: sha512-7teX9DW4tI2WZkXS4DBxneYSY7NHiXl4AKdWDO9LXVweULlCT8OPWsOjLEnMIXViN1j+QcY8mfbq3k0EK6x3uA==} + engines: {node: '>=12.2.0'} + dependencies: + estree-util-is-identifier-name: 1.1.0 + estree-util-value-to-estree: 1.3.0 + js-yaml: 4.1.0 + toml: 3.0.0 + dev: true + + /remark-mdx@2.3.0: + resolution: {integrity: sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==} + dependencies: + mdast-util-mdx: 2.0.1 + micromark-extension-mdxjs: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: true + /remark-parse@10.0.2: resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} dependencies: @@ -24469,6 +25589,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /require-like@0.1.2: + resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} + dev: true + /requireindex@1.2.0: resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} engines: {node: '>=0.10.5'} @@ -24529,6 +25653,11 @@ packages: engines: {node: '>=10'} dev: true + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -25111,6 +26240,10 @@ packages: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true + /set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + dev: true + /setimmediate-napi@1.0.6: resolution: {integrity: sha512-sdNXN15Av1jPXuSal4Mk4tEAKn0+8lfF9Z50/negaQMrAIO9c1qM0eiCh8fT6gctp0RiCObk+6/Xfn5RMGdZoA==} dependencies: @@ -25674,6 +26807,14 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /stream-shift@1.0.1: + resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} + dev: true + + /stream-slice@0.1.2: + resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + dev: true + /stream-throttle@0.1.3: resolution: {integrity: sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==} engines: {node: '>= 0.10.0'} @@ -25757,6 +26898,13 @@ packages: safe-buffer: 5.2.1 dev: true + /stringify-entities@4.0.3: + resolution: {integrity: sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==} + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -26492,6 +27640,10 @@ packages: ieee754: 1.2.1 dev: true + /toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + dev: true + /tough-cookie@2.4.3: resolution: {integrity: sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==} engines: {node: '>=0.8'} @@ -27045,12 +28197,25 @@ packages: '@types/unist': 2.0.6 dev: true + /unist-util-position-from-estree@1.1.2: + resolution: {integrity: sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==} + dependencies: + '@types/unist': 2.0.6 + dev: true + /unist-util-position@4.0.4: resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} dependencies: '@types/unist': 2.0.6 dev: true + /unist-util-remove-position@4.0.2: + resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==} + dependencies: + '@types/unist': 2.0.6 + unist-util-visit: 4.1.2 + dev: true + /unist-util-stringify-position@3.0.3: resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} dependencies: @@ -27551,6 +28716,30 @@ packages: vfile-message: 3.1.4 dev: true + /vite-node@0.28.5(@types/node@18.16.9)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0): + resolution: {integrity: sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==} + engines: {node: '>=v14.16.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4(supports-color@5.5.0) + mlly: 1.4.2 + pathe: 1.1.1 + picocolors: 1.0.0 + source-map: 0.6.1 + source-map-support: 0.5.21 + vite: 4.5.0(@types/node@18.16.9)(less@4.2.0)(sass@1.69.5)(stylus@0.59.0)(terser@5.24.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-node@0.34.6(@types/node@18.16.9)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0): resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} @@ -27794,6 +28983,19 @@ packages: setimmediate-napi: 1.0.6 dev: false + /web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: true + + /web-streams-polyfill@3.2.1: + resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} + engines: {node: '>= 8'} + dev: true + /web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -28330,6 +29532,14 @@ packages: isexe: 2.0.0 dev: true + /which@3.0.1: + resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + /which@4.0.0: resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} engines: {node: ^16.13.0 || >=18.0.0} @@ -28519,6 +29729,11 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: true + /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} diff --git a/scripts/commitizen.js b/scripts/commitizen.js index 6c542ea3aa8e75..0d2fec9496fef9 100644 --- a/scripts/commitizen.js +++ b/scripts/commitizen.js @@ -19,6 +19,7 @@ const scopes = [ { value: 'nx-dev', name: 'nx-dev: anything related to docs infrastructure' }, { value: 'react', name: 'react: anything React specific' }, { value: 'react-native', name: 'react-native: anything React Native specific' }, + { value: 'remix', name: 'remix: anything Remix specific' }, { value: 'expo', name: 'expo: anything Expo specific' }, { value: 'release', name: 'release: anything related to nx release' }, { value: 'repo', name: 'repo: anything related to managing the repo itself' }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 18f8bf5b8d7dde..cdd99a81f12871 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -97,6 +97,8 @@ "@nx/react-native": ["packages/react-native"], "@nx/react-native/*": ["packages/react-native/*"], "@nx/react/*": ["packages/react/*"], + "@nx/remix": ["packages/remix"], + "@nx/remix/*": ["packages/remix/*"], "@nx/rollup": ["packages/rollup"], "@nx/rollup/*": ["packages/rollup/*"], "@nx/storybook": ["packages/storybook"],