diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa9cf71ba..9d8deac80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -192,3 +192,28 @@ BREAKING CHANGE: Dropped support for Next.js 11 and React 16. Users requiring these older versions should stick to v1.6. ``` + +## Local Drupal Environment (Experimental) + +Requires [DDEV](https://ddev.readthedocs.io/en/latest/users/install/ddev-installation/) + +Create a local instance by running: + +``` +yarn ddev:init +``` + +This will create a local Drupal instance in local-next-drupal which will be ignored by git. + +Destroy the local instance by running: + +``` +yarn ddev:destroy +``` + +Things that should work out of the box after running this script: + +- basic starter +- pages starter +- module tests (run from local-next-drupal directory) + - `SIMPLETEST_DB=sqlite://localhost/:memory: ./vendor/bin/phpunit -c ./web/core modules/next` diff --git a/package.json b/package.json index ff011f5ff..0272694ec 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,12 @@ "test": "yarn workspace next-drupal test", "pretest": "yarn format:check && yarn lint", "test:e2e": "turbo run test:e2e --parallel", - "test:e2e:ci": "turbo run test:e2e:ci --parallel" + "test:e2e:ci": "turbo run test:e2e:ci --parallel", + "ddev:init": "./scripts/init-drupal.sh", + "ddev:init:basic": "./scripts/init-drupal.sh --starter basic-starter", + "ddev:init:pages": "./scripts/init-drupal.sh --starter pages-starter", + "ddev:init:graphql": "./scripts/init-drupal.sh --starter graphql-starter", + "ddev:destroy": "./scripts/destroy-drupal.sh" }, "devDependencies": { "@actions/core": "^1.10.1", diff --git a/recipes/next_drupal_base/composer.json b/recipes/next_drupal_base/composer.json new file mode 100644 index 000000000..9286d9163 --- /dev/null +++ b/recipes/next_drupal_base/composer.json @@ -0,0 +1,15 @@ +{ + "name": "drupal/next_drupal_base", + "description": "Common dependencies and configuration for all Next Drupal projects.", + "type": "drupal-recipe", + "license": "GPL-2.0-or-later", + "repositories": [ + { + "type": "composer", + "url": "https://packages.drupal.org/8" + } + ], + "require": { + "drupal/next": "*" + } +} diff --git a/recipes/next_drupal_base/config/pathauto.pattern.content.yml b/recipes/next_drupal_base/config/pathauto.pattern.content.yml new file mode 100644 index 000000000..fd61af24c --- /dev/null +++ b/recipes/next_drupal_base/config/pathauto.pattern.content.yml @@ -0,0 +1,23 @@ +uuid: a5900cb4-ec2f-4788-9e17-73ab49bfcb83 +langcode: en +status: true +dependencies: + module: + - node +id: content +label: Content +type: 'canonical_entities:node' +pattern: '[node:title]' +selection_criteria: + c0c92c0f-9ee6-4b53-8270-198bd024c071: + id: 'entity_bundle:node' + negate: false + uuid: c0c92c0f-9ee6-4b53-8270-198bd024c071 + context_mapping: + node: node + bundles: + article: article + page: page +selection_logic: and +weight: -5 +relationships: { } diff --git a/recipes/next_drupal_base/recipe.yml b/recipes/next_drupal_base/recipe.yml new file mode 100644 index 000000000..4bc1c9ac9 --- /dev/null +++ b/recipes/next_drupal_base/recipe.yml @@ -0,0 +1,7 @@ +name: "Next Drupal Base" +description: "Common dependencies and configuration for all Next Drupal projects" +type: "Site" + +install: + - jsonapi + - next diff --git a/recipes/next_drupal_graphql/composer.json b/recipes/next_drupal_graphql/composer.json new file mode 100644 index 000000000..c18d485e2 --- /dev/null +++ b/recipes/next_drupal_graphql/composer.json @@ -0,0 +1,17 @@ +{ + "name": "drupal/next_drupal_graphql", + "description": "Common dependencies and configuration for Next Drupal GraphQL projects.", + "type": "drupal-recipe", + "license": "GPL-2.0-or-later", + "repositories": [ + { + "type": "composer", + "url": "https://packages.drupal.org/8" + } + ], + "require": { + "drupal/next": "*", + "drupal/graphql": "^4.6", + "drupal/graphql_compose": "^2.0" + } +} diff --git a/recipes/next_drupal_graphql/config/graphql.graphql_servers.graphql_schema.yml b/recipes/next_drupal_graphql/config/graphql.graphql_servers.graphql_schema.yml new file mode 100644 index 000000000..c50edb91a --- /dev/null +++ b/recipes/next_drupal_graphql/config/graphql.graphql_servers.graphql_schema.yml @@ -0,0 +1,18 @@ +uuid: 3954a200-86f3-499b-914e-99a6dac58861 +langcode: en +status: true +dependencies: { } +name: graphql_schema +label: 'Graphql Schema' +endpoint: /graphql +debug_flag: 0 +schema: graphql_compose +caching: true +batching: true +disable_introspection: false +query_depth: null +query_complexity: null +schema_configuration: + graphql_compose: + enabled: true +persisted_queries_settings: { } diff --git a/recipes/next_drupal_graphql/config/graphql_compose.settings.yml b/recipes/next_drupal_graphql/config/graphql_compose.settings.yml new file mode 100644 index 000000000..2a3816cd2 --- /dev/null +++ b/recipes/next_drupal_graphql/config/graphql_compose.settings.yml @@ -0,0 +1,50 @@ +entity_config: + node: + article: + enabled: true + query_load_enabled: true + edges_enabled: true + routes_enabled: true + page: + enabled: true + query_load_enabled: true + edges_enabled: true + routes_enabled: true + taxonomy_term: + tags: + enabled: false + user: + user: + enabled: true + query_load_enabled: true + edges_enabled: true + routes_enabled: true +field_config: + node: + article: + body: + enabled: true + field_image: + enabled: true + field_tags: + enabled: true + page: + body: + enabled: true + user: + user: + user_picture: + enabled: true +settings: + exclude_unpublished: true + expose_entity_ids: false + field_required_override: false + schema_description: "GraphQL Compose" + schema_version: "1" + simple_queries: true + simple_unions: true + site_name: false + site_slogan: false + site_front: true + inflector_langcode: en + inflector_singularize: true diff --git a/recipes/next_drupal_graphql/config/pathauto.pattern.content.yml b/recipes/next_drupal_graphql/config/pathauto.pattern.content.yml new file mode 100644 index 000000000..fd61af24c --- /dev/null +++ b/recipes/next_drupal_graphql/config/pathauto.pattern.content.yml @@ -0,0 +1,23 @@ +uuid: a5900cb4-ec2f-4788-9e17-73ab49bfcb83 +langcode: en +status: true +dependencies: + module: + - node +id: content +label: Content +type: 'canonical_entities:node' +pattern: '[node:title]' +selection_criteria: + c0c92c0f-9ee6-4b53-8270-198bd024c071: + id: 'entity_bundle:node' + negate: false + uuid: c0c92c0f-9ee6-4b53-8270-198bd024c071 + context_mapping: + node: node + bundles: + article: article + page: page +selection_logic: and +weight: -5 +relationships: { } diff --git a/recipes/next_drupal_graphql/config/simple_oauth.oauth2_scope.nextjs_site.yml b/recipes/next_drupal_graphql/config/simple_oauth.oauth2_scope.nextjs_site.yml new file mode 100644 index 000000000..a3794b2d2 --- /dev/null +++ b/recipes/next_drupal_graphql/config/simple_oauth.oauth2_scope.nextjs_site.yml @@ -0,0 +1,22 @@ +uuid: a5bd2fda-4983-4e8d-8019-1c2695b15a42 +langcode: en +status: true +dependencies: {} +id: nextjs_site +name: nextjs_site +description: "Next.js Site" +grant_types: + refresh_token: + status: false + description: "" + client_credentials: + status: true + description: "" + authorization_code: + status: false + description: "" +umbrella: false +parent: _none +granularity_id: role +granularity_configuration: + role: next_js_site diff --git a/recipes/next_drupal_graphql/config/simple_oauth.settings.yml b/recipes/next_drupal_graphql/config/simple_oauth.settings.yml new file mode 100644 index 000000000..46c145673 --- /dev/null +++ b/recipes/next_drupal_graphql/config/simple_oauth.settings.yml @@ -0,0 +1,5 @@ +scope_provider: dynamic +token_cron_batch_size: 0 +public_key: ../keys/public.key +private_key: ../keys/private.key +disable_openid_connect: false diff --git a/recipes/next_drupal_graphql/config/user.role.next_js_site.yml b/recipes/next_drupal_graphql/config/user.role.next_js_site.yml new file mode 100644 index 000000000..31fd02e42 --- /dev/null +++ b/recipes/next_drupal_graphql/config/user.role.next_js_site.yml @@ -0,0 +1,11 @@ +uuid: 9082be18-8b66-4bfa-8d54-e703ef2c7e2d +langcode: en +status: true +dependencies: {} +id: next_js_site +label: "Next.js Site" +weight: 4 +is_admin: false +permissions: + - "execute graphql_schema arbitrary graphql requests" + - "execute graphql_schema persisted graphql requests" diff --git a/recipes/next_drupal_graphql/recipe.yml b/recipes/next_drupal_graphql/recipe.yml new file mode 100644 index 000000000..fdf21dd50 --- /dev/null +++ b/recipes/next_drupal_graphql/recipe.yml @@ -0,0 +1,11 @@ +name: "Next Drupal GraphQL" +description: "Common dependencies and configuration for Next Drupal GraphQL projects" +type: "Site" + +install: + - next + - next_graphql + - graphql + - graphql_compose_edges + - graphql_compose_routes + - graphql_compose_users diff --git a/scripts/config/.ddev/config.yaml b/scripts/config/.ddev/config.yaml new file mode 100644 index 000000000..932afe08a --- /dev/null +++ b/scripts/config/.ddev/config.yaml @@ -0,0 +1,31 @@ +name: local-next-drupal +type: drupal +docroot: drupal/web +php_version: "8.3" +webserver_type: nginx-fpm +xdebug_enabled: false +additional_hostnames: + - frontend +additional_fqdns: [] +database: + type: mariadb + version: "10.11" +use_dns_when_possible: true +hooks: + post-start: + - exec: | + composer create drupal/recommended-project tmp + mv -n tmp/* drupal + rm -rf tmp + cd starters/${STARTER_NAME} && npm install && pm2 start "npm run dev --experimental-https" +composer_version: "2" +composer_root: drupal +web_environment: + - STARTER_NAME=@STARTER_NAME +corepack_enable: true +web_extra_exposed_ports: + - name: frontend + container_port: 3000 + http_port: 3080 + https_port: 3433 +nodejs_version: "22" diff --git a/scripts/config/.ddev/web-build/Dockerfile b/scripts/config/.ddev/web-build/Dockerfile new file mode 100644 index 000000000..451fce0d0 --- /dev/null +++ b/scripts/config/.ddev/web-build/Dockerfile @@ -0,0 +1 @@ +RUN npm install -g pm2 diff --git a/scripts/config/.env.local b/scripts/config/.env.local new file mode 100644 index 000000000..32590f42a --- /dev/null +++ b/scripts/config/.env.local @@ -0,0 +1,14 @@ +# See https://next-drupal.org/docs/environment-variables + +# Required +NEXT_PUBLIC_DRUPAL_BASE_URL=https://local-next-drupal.ddev.site +NEXT_IMAGE_DOMAIN=local-next-drupal.ddev.site + +# Authentication +DRUPAL_CLIENT_ID=next_consumer +DRUPAL_CLIENT_SECRET=secret + +# Required for On-demand Revalidation +# DRUPAL_REVALIDATE_SECRET=Retrieve this from /admin/config/services/next + +NODE_TLS_REJECT_UNAUTHORIZED=0 \ No newline at end of file diff --git a/scripts/config/consumers.php b/scripts/config/consumers.php new file mode 100644 index 000000000..d12d448bc --- /dev/null +++ b/scripts/config/consumers.php @@ -0,0 +1,49 @@ +getStorage('consumer'); + +$previewerClientId = Crypt::randomBytesBase64(); +$previewerClientSecret = $random->word(8); +$consumerStorage->create([ + 'client_id' => 'next_consumer', + 'client_secret ' => 'secret', + 'label' => 'Next Consumer', + 'user_id' => 1, + 'third_party' => TRUE, + 'is_default' => FALSE, +])->save(); + +$directory = '../keys'; + +if (create_directory($directory)) { + echo 'Keys dir created succesfully' . PHP_EOL; +} else { + echo 'Failed to create directory' . PHP_EOL; +} + +\Drupal::service('simple_oauth.key.generator')->generateKeys($directory); + +function create_directory($directory) +{ + // Get the file system service. + $file_system = \Drupal::service('file_system'); + + // Check if the directory exists and create it if it doesn't. + if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) { + return FALSE; + } + + return TRUE; +} \ No newline at end of file diff --git a/scripts/config/graphQLConsumer.php b/scripts/config/graphQLConsumer.php new file mode 100644 index 000000000..870ac9e34 --- /dev/null +++ b/scripts/config/graphQLConsumer.php @@ -0,0 +1,61 @@ +getStorage('consumer'); + +// Create a user with role next_js +$user = \Drupal::service('entity_type.manager')->getStorage('user')->create([ + 'name' => 'next', + 'mail' => 'next@ddev.site', + 'password' => 'next', + 'roles' => ['next_js_site'], + 'status' => 1, +]); +$user->save(); + +$previewerClientId = Crypt::randomBytesBase64(); +$previewerClientSecret = $random->word(8); +$consumer = $consumerStorage->create([ + 'client_id' => 'next_consumer', + 'secret' => 'secret', + 'label' => 'Next.js site', + 'user_id' => $user->id(), + 'grant_types' => ['client_credentials'], + 'scopes' => ['nextjs_site'], + 'third_party' => TRUE, + 'is_default' => FALSE, +])->save(); + +$directory = '../keys'; + +if (create_directory($directory)) { + echo 'Keys dir created succesfully' . PHP_EOL; +} else { + echo 'Failed to create directory' . PHP_EOL; +} + +\Drupal::service('simple_oauth.key.generator')->generateKeys($directory); + +function create_directory($directory) +{ + // Get the file system service. + $file_system = \Drupal::service('file_system'); + + // Check if the directory exists and create it if it doesn't. + if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) { + return FALSE; + } + + return TRUE; +} diff --git a/scripts/destroy-drupal.sh b/scripts/destroy-drupal.sh new file mode 100755 index 000000000..e135d9244 --- /dev/null +++ b/scripts/destroy-drupal.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Exit immediately on errors. +set -e + +cd local-next-drupal +ddev delete -y +cd .. +echo "Removing drupal directory..." +rm -rf local-next-drupal diff --git a/scripts/init-drupal.sh b/scripts/init-drupal.sh new file mode 100755 index 000000000..4fad06029 --- /dev/null +++ b/scripts/init-drupal.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +# Exit immediately on errors. +set -e + +# Parse command-line arguments +STARTER_NAME="basic-starter" +VALID_STARTERS=("basic-starter" "pages-starter" "graphql-starter") + +while [[ "$#" -gt 0 ]]; do + case $1 in + --starter) + shift + STARTER_NAME="$1" + ;; + *) + echo "Unknown parameter passed: $1" + exit 1 + ;; + esac + shift +done + +# Validate starter name +is_valid_starter=false +for starter in "${VALID_STARTERS[@]}"; do + if [[ "$STARTER_NAME" == "$starter" ]]; then + is_valid_starter=true + break + fi +done + +if [[ "$is_valid_starter" == false ]]; then + echo "Error: Invalid starter name. Valid starters are: ${VALID_STARTERS[*]}" + exit 1 +fi + +echo "" +echo "****************************************************************************************" +echo "* *" +echo "* Starting Next Drupal for $STARTER_NAME... *" +echo "* *" +echo "****************************************************************************************" +echo "" + +# Copy env file to the specified starter +cp scripts/config/.env.local "starters/$STARTER_NAME" + +# Create DDEV project +mkdir local-next-drupal +cd local-next-drupal + +# Set environment variable for DDEV +export STARTER_NAME=$STARTER_NAME + +# Copy the starters folder. +cp -r ../starters . + +# Add the ddev config. +mkdir .ddev +cp ../scripts/config/.ddev/config.yaml .ddev/config.yaml +cp -R ../scripts/config/.ddev/web-build .ddev +# cp ../scripts/config/.ddev/docker-compose.frontend.yaml .ddev/docker-compose.frontend.yaml +# Replace @STARTER_NAME with actual starter name +sed -i '' -e "s/@STARTER_NAME/$STARTER_NAME/g" .ddev/config.yaml + +ddev start + +# Prevent composer scaffolding from overwriting development.services.yml +ddev composer config --json extra.drupal-scaffold.file-mapping '{"[web-root]/sites/development.services.yml": false}' +ddev composer config minimum-stability dev +ddev composer config allow-plugins.ewcomposer/unpack true -n +ddev composer config allow-plugins.tbachert/spi true -n + +# Add repositories +ddev composer config repositories.unpack vcs https://gitlab.ewdev.ca/yonas.legesse/drupal-recipe-unpack.git +ddev composer config repositories.recipe path web/recipes/next_drupal_base +ddev composer config repositories.recipe path web/recipes/next_drupal_graphql +ddev composer config repositories.next path modules/next + +# Add configuration scripts to run after install +mkdir drupal/scripts +cp ../scripts/config/consumers.php drupal/scripts/consumers.php +cp ../scripts/config/graphQLConsumer.php drupal/scripts/graphQLConsumer.php + +# Use the local repo version of the next module +# ideally this would be a symlink. +cp -a ../modules/. drupal/modules + +# Add recipies +cp -a ../recipes/. drupal/web/recipes + +# Add useful composer dependencies +# if staters is basic or pages +if [[ "$STARTER_NAME" == "basic-starter" || "$STARTER_NAME" == "pages-starter" ]]; then + ddev composer require drush/drush drupal/next_drupal_base drupal/devel drupal/core-dev ewcomposer/unpack:dev-master +else + ddev composer require drush/drush drupal/next_drupal_graphql drupal/devel drupal/core-dev ewcomposer/unpack:dev-master +fi + +# Install Drupal +ddev drush site:install --account-name=admin --account-pass=admin -y + +# if staters is basic or pages +if [[ "$STARTER_NAME" == "basic-starter" || "$STARTER_NAME" == "pages-starter" ]]; then + # Apply recipe + ddev exec -d /var/www/html/drupal/web php core/scripts/drupal recipe recipes/next_drupal_base + ddev composer unpack drupal/next_drupal_base +else + # Apply recipe + ddev exec -d /var/www/html/drupal/web php core/scripts/drupal recipe recipes/next_drupal_graphql + ddev composer unpack drupal/next_drupal_graphql +fi + +# Create example consumer +if [[ "$STARTER_NAME" == "basic-starter" || "$STARTER_NAME" == "pages-starter" ]]; then + ddev drush php:script drupal/scripts/consumers +else + ddev drush php:script drupal/scripts/graphQLConsumer +fi + + +# Generate content +ddev drush pm:enable devel_generate -y +ddev drush genc 25 + +ddev drush cr + +# use the one-time link (CTRL/CMD + Click) from the command below to edit your admin account details. +ddev drush uli | xargs open + +# Exit drupal folder +ddev describe + +cd .. +