├── .nvmrc ├── .npmrc ├── plugin-templates ├── config │ ├── post-meta.json │ └── README.md ├── blocks │ ├── README.md │ ├── theme-navigation │ │ ├── index.scss │ │ ├── index.ts │ │ ├── style.scss │ │ ├── index.php │ │ ├── block.json │ │ ├── README.md │ │ ├── render.php │ │ └── edit.tsx │ ├── theme-search-meta │ │ ├── index.scss │ │ ├── style.scss │ │ ├── index.ts │ │ ├── block.json │ │ ├── index.php │ │ ├── edit.tsx │ │ └── render.php │ ├── theme-faceted-search │ │ ├── index.scss │ │ ├── style.scss │ │ ├── index.php │ │ ├── block.json │ │ ├── index.tsx │ │ ├── render.php │ │ └── edit.tsx │ ├── theme-post-subheadline │ │ ├── index.scss │ │ ├── style.scss │ │ ├── index.ts │ │ ├── index.php │ │ ├── block.json │ │ ├── edit.tsx │ │ └── render.php │ ├── theme-faceted-search-facets │ │ ├── index.scss │ │ ├── src │ │ │ ├── index.ts │ │ │ └── autosubmit.ts │ │ ├── index.ts │ │ ├── index.php │ │ ├── style.scss │ │ ├── block.json │ │ ├── edit.tsx │ │ └── render.php │ └── theme-primary-term │ │ ├── style.scss │ │ ├── index.ts │ │ ├── index.php │ │ ├── block.json │ │ ├── render.php │ │ └── edit.tsx ├── entries │ ├── README.md │ └── slotfills │ │ ├── index.ts │ │ ├── subheadline │ │ ├── index.tsx │ │ └── Subheadline.tsx │ │ ├── featured-image-caption │ │ ├── index.tsx │ │ └── FeaturedImageCaption.tsx │ │ └── index.php ├── src │ └── features │ │ ├── README.md │ │ ├── class-search-customizations.php │ │ ├── class-subheadline.php │ │ ├── class-featured-image-caption.php │ │ ├── class-msm-sitemap-integration.php │ │ └── class-primary-term-rest.php ├── features.txt ├── README.md └── .phpcs.xml ├── plugins └── index.php ├── themes └── index.php ├── mu-plugins ├── index.php ├── plugin-loader.php ├── 000-wp-environment.php └── 001-composer.php ├── Makefile ├── composer-templates ├── auth.json ├── suggested.json ├── pantheon.json └── default.json ├── custom.d.ts ├── .github ├── CODEOWNERS └── workflows │ └── action.yml ├── .eslintrc.js ├── .stylelintrc.json ├── .editorconfig ├── .deployignore ├── tsconfig.json ├── CONTRIBUTORS.md ├── turbo.json ├── jest.config.ts ├── phpstan.neon ├── ci-templates └── .github │ └── workflows │ ├── copy-to-vip.yml │ ├── deploy-to-vip-built-branch.yml │ ├── deploy-to-pantheon.yml │ └── all-pr-tests.yml ├── .gitignore ├── packages └── README.md ├── .phpcs.xml ├── README.md ├── package.json ├── composer.json ├── CONTRIBUTING.md ├── LICENSE └── configure.php /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /plugin-templates/config/post-meta.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/index.php: -------------------------------------------------------------------------------- 1 | >; 5 | const src: string; 6 | export default src; 7 | } 8 | -------------------------------------------------------------------------------- /plugin-templates/entries/slotfills/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry for Gutenberg Slotfills 3 | * 4 | * Register slotfills in child folders under the current directory and import 5 | * them here. 6 | */ 7 | 8 | import './featured-image-caption'; 9 | import './subheadline'; 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file assigns responsibility for reviewing specific files in this repo to specific people or teams. 2 | # See: https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | * @alleyinteractive/admins 5 | -------------------------------------------------------------------------------- /plugin-templates/entries/slotfills/subheadline/index.tsx: -------------------------------------------------------------------------------- 1 | import { registerPlugin } from '@wordpress/plugins'; 2 | 3 | import Subheadline from './Subheadline'; 4 | 5 | registerPlugin( 6 | 'create-wordpress-plugin-subheadline', 7 | { 8 | render: Subheadline, 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@alleyinteractive/eslint-config/typescript-react'], 4 | parserOptions: { 5 | project: true, 6 | tsconfigRootDir: __dirname, 7 | }, 8 | rules: { 9 | 'import/no-extraneous-dependencies': [0], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@alleyinteractive/stylelint-config", 3 | "overrides": [ 4 | { 5 | "files": [ 6 | "*.module.scss" 7 | ], 8 | "rules": { 9 | "selector-class-pattern": "(^[a-z]|[A-Z0-9])[a-z]*" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search-facets/src/index.ts: -------------------------------------------------------------------------------- 1 | import domReady from '@wordpress/dom-ready'; 2 | import autosubmit from './autosubmit'; 3 | 4 | /** 5 | * Initialize faceted search functions. 6 | */ 7 | function init() { 8 | autosubmit(); 9 | } 10 | 11 | domReady(() => init()); 12 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-navigation/index.ts: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | 3 | import edit from './edit'; 4 | import metadata from './block.json'; 5 | 6 | import './style.scss'; 7 | 8 | /* @ts-expect-error Provided types are inaccurate to the actual plugin API. */ 9 | registerBlockType(metadata, { edit }); 10 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-primary-term/index.ts: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | 3 | import edit from './edit'; 4 | import metadata from './block.json'; 5 | 6 | import './style.scss'; 7 | 8 | /* @ts-expect-error Provided types are inaccurate to the actual plugin API. */ 9 | registerBlockType(metadata, { edit }); 10 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-search-meta/index.ts: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | 3 | import edit from './edit'; 4 | import metadata from './block.json'; 5 | 6 | import './style.scss'; 7 | 8 | /* @ts-expect-error Provided types are inaccurate to the actual plugin API. */ 9 | registerBlockType(metadata, { edit }); 10 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-post-subheadline/index.ts: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | 3 | import edit from './edit'; 4 | import metadata from './block.json'; 5 | 6 | import './style.scss'; 7 | 8 | /* @ts-expect-error Provided types are inaccurate to the actual plugin API. */ 9 | registerBlockType(metadata, { edit }); 10 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search-facets/index.ts: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | 3 | import edit from './edit'; 4 | import metadata from './block.json'; 5 | 6 | import './style.scss'; 7 | 8 | /* @ts-expect-error Provided types are inaccurate to the actual plugin API. */ 9 | registerBlockType(metadata, { edit }); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | 11 | [*.{ts,tsx,js,jsx,scss,css,json,yaml,yml,feature,xml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | # Dotfiles 16 | [.*] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.deployignore: -------------------------------------------------------------------------------- 1 | # Caches 2 | .npm 3 | .phpcs.cache.json 4 | .phpunit.result.cache 5 | 6 | # Dependencies 7 | node_modules 8 | 9 | # IDE Config Directories 10 | *.code-workspace 11 | .idea 12 | .vscode 13 | 14 | # OS-Managed Files 15 | .DS_Store 16 | .DS_Store? 17 | .Spotlight-V100 18 | .Trashes 19 | ehthumbs.db 20 | Thumbs.db 21 | .thumbsdb 22 | 23 | # WordPress-Managed Files 24 | /db.php 25 | /upgrade/ 26 | /uploads/ 27 | -------------------------------------------------------------------------------- /composer-templates/suggested.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ES Admin", 4 | "path": "alleyinteractive/es-admin" 5 | }, 6 | { 7 | "name": "ES WP Query", 8 | "path": "alleyinteractive/es-wp-query" 9 | }, 10 | { 11 | "name": "Fieldmanager", 12 | "path": "alleyinteractive/wordpress-fieldmanager" 13 | }, 14 | { 15 | "name": "Yoast Duplicate Post", 16 | "path": "wpackagist-plugin/duplicate-post" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /plugin-templates/features.txt: -------------------------------------------------------------------------------- 1 | $plugin = new Group( 2 | new Features\Featured_Image_Caption(), 3 | new Features\Subheadline(), 4 | new Features\Search_Customizations( 5 | post_types: [ 'post', 'page' ], 6 | taxonomies: [ 'category' ], 7 | ), 8 | new Features\MSM_Sitemap_Integration( 9 | post_types: [ 'post' ], 10 | ), 11 | new Features\Primary_Term_Rest( 12 | taxonomies: [ 'category', 'post_tag' ], 13 | ), 14 | ); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@alleyinteractive/tsconfig/base.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["*"] 8 | } 9 | }, 10 | "exclude": [ 11 | "./plugins/create-wordpress-plugin/build", 12 | "./themes/create-wordpress-theme/build" 13 | ], 14 | "include": [ 15 | "./custom.d.ts", 16 | "./jest.config.ts", 17 | "./plugins/create-wordpress-plugin/**/*", 18 | "./themes/create-wordpress-theme/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | This project exists thanks to all the people who contribute. 4 | 5 | Thank you to everyone who has contributed to this project! Instead of maintaining a manual list that may become outdated, we invite you to check out the [GitHub contributors page](https://github.com/alleyinteractive/create-wordpress-project/graphs/contributors) for a complete and up-to-date list of all the amazing people who have helped make this project possible. 6 | 7 | If you are interested in contributing, please read our [contributing guidelines](CONTRIBUTING.md). 8 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-primary-term/index.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | { __("7 results for 'search term'") } 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search-facets/index.php: -------------------------------------------------------------------------------- 1 | /$1', 6 | }, 7 | preset: 'ts-jest', 8 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], 9 | testEnvironment: 'jsdom', 10 | testMatch: [ 11 | '**/plugins/create-wordpress-plugin/**/__tests__/**/*.ts?(x)', 12 | '**/themes/create-wordpress-theme/**/__tests__/**/*.ts?(x)', 13 | '**/plugins/create-wordpress-plugin/**/?(*.)+(spec|test).ts?(x)', 14 | '**/themes/create-wordpress-theme/**/?(*.)+(spec|test).ts?(x)', 15 | ], 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /plugin-templates/README.md: -------------------------------------------------------------------------------- 1 | # Plugin Templates 2 | 3 | The plugin-templates folder should contain subfolder that should be copied to the plugin folder after it is scaffolded. Search and replace will be run on the files after they are copied over. 4 | 5 | Currently the subfolders are 6 | * blocks 7 | * config 8 | * entries 9 | * features 10 | 11 | Use the following placeholders in your files, and they will automatically get updated to the correct values to match the destination plugin. 12 | 13 | * `create-wordpress-plugin` 14 | * `Create WordPress Plugin` 15 | * `CREATE_WORDPRESS_PLUGIN` 16 | * `create_wordpress_plugin` 17 | * `Create_WordPress_Plugin` 18 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "create-wordpress-plugin/theme-faceted-search", 5 | "version": "0.1.0", 6 | "title": "Faceted Search", 7 | "category": "theme", 8 | "icon": "search", 9 | "description": "Search results wrapper for use with search facets.", 10 | "textdomain": "create-wordpress-plugin", 11 | "editorScript": "file:index.tsx", 12 | "editorStyle": "file:index.css", 13 | "style": [ 14 | "file:style-index.css" 15 | ], 16 | "render": "file:render.php", 17 | "supports": { 18 | "layout": true, 19 | "multiple": false 20 | } 21 | } -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - mu-plugins/vendor/szepeviktor/phpstan-wordpress/extension.neon 3 | 4 | parameters: 5 | # Level 9 is the highest level 6 | level: max 7 | 8 | scanDirectories: 9 | - plugins/elasticsearch-extensions 10 | - plugins/wp-asset-manager 11 | 12 | paths: 13 | - mu-plugins/000-wp-environment.php 14 | - mu-plugins/001-composer.php 15 | - mu-plugins/plugin-loader.php 16 | - plugins/create-wordpress-plugin/ 17 | - themes/create-wordpress-theme/ 18 | 19 | excludePaths: 20 | - plugins/create-wordpress-plugin/build/ 21 | - plugins/create-wordpress-plugin/tests/ 22 | - themes/create-wordpress-theme/build/ 23 | - themes/create-wordpress-theme/tests/ 24 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-post-subheadline/edit.tsx: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | import { useBlockProps } from '@wordpress/block-editor'; 3 | import { ReactElement } from 'react'; 4 | 5 | import './index.scss'; 6 | 7 | type EditProps = { 8 | context: { 9 | postId: number; 10 | }; 11 | }; 12 | 13 | /** 14 | * The create-wordpress-plugin/theme-post-subheadline block edit function. 15 | * 16 | * @return {WPElement} Element to render. 17 | */ 18 | export default function Edit({ 19 | context: { postId }, 20 | }: EditProps): ReactElement { 21 | return ( 22 |

23 | { __('Subheading') } 24 | { postId } 25 |

26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search/index.tsx: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; 3 | 4 | import edit from './edit'; 5 | import metadata from './block.json'; 6 | 7 | import './style.scss'; 8 | 9 | registerBlockType( 10 | /* @ts-expect-error Provided types are inaccurate to the actual plugin API. */ 11 | metadata, 12 | { 13 | edit, 14 | save: () => { 15 | const blockProps = useBlockProps.save(); 16 | return ( 17 |
18 | {/* @ts-ignore */} 19 | 20 |
21 | ); 22 | }, 23 | }, 24 | ); 25 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search-facets/src/autosubmit.ts: -------------------------------------------------------------------------------- 1 | export default function autosubmit() { 2 | const facetContainer = document.querySelector('.wp-block-create-wordpress-plugin-theme-faceted-search-facets'); 3 | if (!facetContainer) { 4 | return; 5 | } 6 | 7 | const form = facetContainer.closest('form'); 8 | if (!form) { 9 | return; 10 | } 11 | 12 | const checkboxes = facetContainer.querySelectorAll('input[type="checkbox"]'); 13 | for (const checkbox of checkboxes) { 14 | checkbox.addEventListener('change', (event) => { 15 | if (event.target && event.target instanceof HTMLInputElement) { 16 | form.submit(); 17 | } 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search-facets/style.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Styles for the create-wordpress-plugin/theme-faceted-search-facets block that get applied on both on the front of your site 3 | * and in the editor. 4 | */ 5 | 6 | .wp-block-create-wordpress-plugin-theme-faceted-search-facets { 7 | fieldset { 8 | border: unset; 9 | margin-block: var(--wp--custom--spacing--block-gap, 1.5rem); 10 | margin-inline: unset; 11 | padding: unset; 12 | } 13 | 14 | legend { 15 | display: block; 16 | font-weight: 700; 17 | margin-block-end: 0.5rem; 18 | } 19 | 20 | label { 21 | display: block; 22 | } 23 | } 24 | 25 | .wp-block-create-wordpress-plugin-theme-faceted-search-facets__heading { 26 | margin-block-start: 0; 27 | } 28 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-navigation/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "create-wordpress-plugin/theme-navigation", 5 | "version": "0.1.0", 6 | "title": "Theme Navigation", 7 | "category": "theme", 8 | "icon": "menu", 9 | "description": "Display theme navigation menus.", 10 | "textdomain": "create-wordpress-plugin", 11 | "editorScript": "file:index.ts", 12 | "editorStyle": "file:index.css", 13 | "style": [ 14 | "file:style-index.css" 15 | ], 16 | "render": "file:render.php", 17 | "attributes": { 18 | "menuLocation": { 19 | "type": "string", 20 | "default": "" 21 | } 22 | }, 23 | "supports": { 24 | "align": [ "wide", "full" ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search-facets/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "create-wordpress-plugin/theme-faceted-search-facets", 5 | "version": "0.1.0", 6 | "title": "Search Facets", 7 | "category": "theme", 8 | "icon": "filter", 9 | "description": "Displays search facets. Elasticsearch Extensions must be activated.", 10 | "textdomain": "create-wordpress-plugin", 11 | "editorScript": "file:index.ts", 12 | "editorStyle": "file:index.css", 13 | "viewScript": [ 14 | "file:./src/index.ts" 15 | ], 16 | "style": [ 17 | "file:style-index.css" 18 | ], 19 | "ancestor": [ 20 | "create-wordpress-plugin/theme-faceted-search" 21 | ], 22 | "render": "file:render.php" 23 | } -------------------------------------------------------------------------------- /plugin-templates/entries/slotfills/featured-image-caption/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds a "Featured Image Caption" option to the featured image box. 3 | */ 4 | import { addFilter } from '@wordpress/hooks'; 5 | import JSXElement from 'react'; 6 | import FeaturedImageCaption from './FeaturedImageCaption'; 7 | 8 | /* @ts-ignore - JSXElement not recognized */ 9 | function featuredImageCaption(OriginalComponent: JSXElement) { 10 | return function (props: object) { /* eslint-disable-line func-names */ 11 | return ( 12 | <> 13 | 14 | 15 | 16 | ); 17 | }; 18 | } 19 | 20 | addFilter( 21 | 'editor.PostFeaturedImage', 22 | 'create-wordpress-plugin/featured-image-caption', 23 | featuredImageCaption, 24 | ); 25 | -------------------------------------------------------------------------------- /plugin-templates/entries/slotfills/featured-image-caption/FeaturedImageCaption.tsx: -------------------------------------------------------------------------------- 1 | import { TextareaControl } from '@wordpress/components'; 2 | import { usePostMetaValue } from '@alleyinteractive/block-editor-tools'; 3 | import { __ } from '@wordpress/i18n'; 4 | 5 | function FeaturedImageCaption() { 6 | const [featuredImageCaption, setFeaturedImageCaption] = usePostMetaValue('create_wordpress_plugin_featured_image_caption'); 7 | 8 | return ( 9 | <> 10 |
11 | { setFeaturedImageCaption(value); }} 16 | /> 17 | 18 | ); 19 | } 20 | 21 | export default FeaturedImageCaption; 22 | -------------------------------------------------------------------------------- /composer-templates/pantheon.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Pantheon Advanced Page Cache", 4 | "path": "wpackagist-plugin/pantheon-advanced-page-cache" 5 | }, 6 | { 7 | "name": "Query Monitor", 8 | "path": "wpackagist-plugin/query-monitor" 9 | }, 10 | { 11 | "name": "Rewrite Rules Inspector", 12 | "path": "wpackagist-plugin/rewrite-rules-inspector" 13 | }, 14 | { 15 | "name": "SearchPress", 16 | "path": "alleyinteractive/searchpress" 17 | }, 18 | { 19 | "name": "SearchPress Debug Bar", 20 | "path": "alleyinteractive/debug-bar-searchpress" 21 | }, 22 | { 23 | "name": "Two Factor", 24 | "path": "wpackagist-plugin/two-factor" 25 | }, 26 | { 27 | "name": "WP Mail SMTP", 28 | "path": "wpackagist-plugin/wp-mail-smtp", 29 | "activate": false 30 | }, 31 | { 32 | "name": "WP Native PHP Sessions", 33 | "path": "wpackagist-plugin/wp-native-php-sessions" 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-navigation/README.md: -------------------------------------------------------------------------------- 1 | # Theme Navigation 2 | Allows users to display a menu that is attached to a specific menu location. 3 | 4 | ## Usage 5 | 1. Register menu location. 6 | 2. Create a menu and assign it to a menu location. 7 | 3. Add the block to a page and select the menu location to display via the block settings. 8 | 9 | ## Styling 10 | Each block is rendered with a data attribute of the location that is being targeted. This allows 11 | for custom styling of each menu location. 12 | 13 | For example, targeting the menu location `header` would look like this: 14 | 15 | ```css 16 | .wp-block-create-wordpress-plugin-theme-navigation[data-location="header"] { 17 | background-color: #0a4b78; 18 | } 19 | ``` 20 | 21 | ## Props 22 | | Name | Type | Default | Required | Description | 23 | |--------------|--------|---------|----------|------------------------------| 24 | | menuLocation | string | | No | The menu location to target. | 25 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search/render.php: -------------------------------------------------------------------------------- 1 | $attributes 10 | * 11 | * @var array $attributes The array of attributes for this block. 12 | * @var string $content Rendered block output. ie. . 13 | * @var WP_Block $block The instance of the WP_Block class that represents the block being rendered. 14 | * 15 | * @package create-wordpress-plugin 16 | */ 17 | 18 | ?> 19 | 22 | -------------------------------------------------------------------------------- /ci-templates/.github/workflows/copy-to-vip.yml: -------------------------------------------------------------------------------- 1 | name: Copy Branch to VIP Repository 2 | 3 | on: 4 | push: 5 | branches: 6 | - production 7 | - preprod 8 | - develop 9 | 10 | jobs: 11 | build-and-sync: 12 | if: github.repository == 'alleyinteractive/create-wordpress-project' 13 | runs-on: ubuntu-latest 14 | name: "Copy Branch to VIP Repository" 15 | timeout-minutes: 1 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Sync to wpcomvip repo 21 | uses: alleyinteractive/action-deploy-to-remote-repository@v3 22 | with: 23 | deployignore: 'false' 24 | exclude_list: '.git, .gitmodules, node_modules, no-vip' 25 | remote_repo: 'git@github.com:wpcomvip/VIP_REPO_SLUG.git' 26 | ssh-key: ${{ secrets.REMOTE_REPO_SSH_KEY }} 27 | 28 | - name: Notify Slack 29 | if: always() 30 | uses: alleyinteractive/action-notify-slack@develop 31 | with: 32 | status: ${{ job.status }} 33 | webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} 34 | -------------------------------------------------------------------------------- /composer-templates/default.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Alleyvate", 4 | "path": "alleyinteractive/wp-alleyvate" 5 | }, 6 | { 7 | "name": "Byline Manager", 8 | "path": "alleyinteractive/byline-manager" 9 | }, 10 | { 11 | "name": "Elasticsearch Extensions", 12 | "path": "alleyinteractive/elasticsearch-extensions" 13 | }, 14 | { 15 | "name": "Meta Inspector", 16 | "path": "alleyinteractive/meta-inspector" 17 | }, 18 | { 19 | "name": "MSM Sitemap", 20 | "path": "Automattic/msm-sitemap" 21 | }, 22 | { 23 | "name": "Safe Redirect Manager", 24 | "path": "wpackagist-plugin/safe-redirect-manager" 25 | }, 26 | { 27 | "name": "WP Asset Manager", 28 | "path": "alleyinteractive/wp-asset-manager" 29 | }, 30 | { 31 | "name": "WP Curate", 32 | "path": "alleyinteractive/wp-curate" 33 | }, 34 | { 35 | "name": "WP New Relic Transactions", 36 | "path": "alleyinteractive/wp-new-relic-transactions" 37 | }, 38 | { 39 | "name": "Yoast SEO", 40 | "path": "wpackagist-plugin/wordpress-seo" 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /plugin-templates/.phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PHP_CodeSniffer standard for Create WordPress Plugin 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-navigation/render.php: -------------------------------------------------------------------------------- 1 | $attributes The array of attributes for this block. 8 | * @var string $content Rendered block output. ie. . 9 | * @var WP_Block $block The instance of the WP_Block class that represents the block being rendered. 10 | * 11 | * @package create-wordpress-plugin 12 | */ 13 | 14 | $menu_location = isset( $attributes['menuLocation'] ) && is_string( $attributes['menuLocation'] ) 15 | ? $attributes['menuLocation'] 16 | : ''; 17 | 18 | if ( empty( $menu_location ) ) { 19 | return; 20 | } 21 | 22 | ?> 23 | 34 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-primary-term/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "create-wordpress-plugin/theme-primary-term", 5 | "version": "0.1.0", 6 | "title": "Primary Term", 7 | "category": "theme", 8 | "icon": "category", 9 | "description": "Displays the primary term as set by Yoast or the first term", 10 | "textdomain": "create-wordpress-plugin", 11 | "editorScript": "file:index.ts", 12 | "editorStyle": "file:index.css", 13 | "style": [ 14 | "file:style-index.css" 15 | ], 16 | "render": "file:render.php", 17 | "usesContext": [ 18 | "postId" 19 | ], 20 | "attributes": { 21 | "isLink": { 22 | "type": "boolean", 23 | "default": true 24 | }, 25 | "taxonomy": { 26 | "type": "string", 27 | "default": "category" 28 | } 29 | }, 30 | "variations": [ 31 | { 32 | "name": "default", 33 | "title": "Primary Category", 34 | "isDefault": true, 35 | "attributes": { 36 | "taxonomy": "category" 37 | } 38 | }, 39 | { 40 | "name": "post_tag", 41 | "title": "Primary Tag", 42 | "attributes": { 43 | "taxonomy": "post_tag" 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /mu-plugins/plugin-loader.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | function create_wordpress_project_local_plugins(): array { 20 | if ( wp_get_environment_type() !== 'local' ) { 21 | return []; 22 | } 23 | 24 | return [ 25 | 'create-block-theme/create-block-theme.php' 26 | ]; 27 | } 28 | 29 | /** 30 | * Returns a list of plugin main file paths (under the plugins directory) to load via code. 31 | * 32 | * @return array 33 | */ 34 | function create_wordpress_project_core_plugins(): array { 35 | return array_merge( 36 | [ 37 | 'create-wordpress-plugin/create-wordpress-plugin.php', 38 | ], 39 | create_wordpress_project_local_plugins() 40 | ); 41 | } 42 | 43 | new WP_Plugin_Loader( create_wordpress_project_core_plugins() ); 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # BEGIN DELETE AFTER INSTALL # 2 | composer.lock 3 | # END DELETE AFTER INSTALL # 4 | 5 | # Caches 6 | .turbo 7 | .npm 8 | .phpcs.cache.json 9 | .phpunit.result.cache 10 | 11 | # Dependencies 12 | node_modules 13 | vendor 14 | 15 | # IDE Config Directories 16 | *.code-workspace 17 | .idea 18 | .vscode 19 | 20 | # OS-Managed Files 21 | .DS_Store 22 | .DS_Store? 23 | .Spotlight-V100 24 | .Trashes 25 | ehthumbs.db 26 | Thumbs.db 27 | .thumbsdb 28 | 29 | # WordPress-Managed Files 30 | /db.php 31 | /object-cache.php 32 | /upgrade/ 33 | /uploads/ 34 | 35 | # Ignore all mu-plugins except our custom ones 36 | /mu-plugins/* 37 | !/mu-plugins/000-wp-environment.php 38 | !/mu-plugins/001-composer.php 39 | !/mu-plugins/index.php 40 | !/mu-plugins/plugin-loader.php 41 | 42 | # Ignore all plugins except our custom ones 43 | /plugins/* 44 | !/plugins/index.php 45 | !/plugins/create-wordpress-plugin 46 | 47 | # Ignore all themes except our custom ones 48 | /themes/* 49 | !/themes/index.php 50 | !/themes/create-wordpress-theme 51 | 52 | # Ignore custom build directories 53 | /plugins/create-wordpress-plugin/build 54 | /themes/create-wordpress-theme/build 55 | 56 | Ignore known log files 57 | debug.log 58 | -------------------------------------------------------------------------------- /mu-plugins/000-wp-environment.php: -------------------------------------------------------------------------------- 1 | 'staging', 25 | 'develop' => 'development', 26 | 'live', 'production' => 'production', 27 | default => 'local', 28 | }, 29 | ); 30 | 31 | // Don't pollute global scope. 32 | unset( $environment_source ); 33 | } 34 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-post-subheadline/render.php: -------------------------------------------------------------------------------- 1 | $attributes 10 | * 11 | * @var array $attributes The array of attributes for this block. 12 | * @var string $content Rendered block output. ie. . 13 | * @var WP_Block $block The instance of the WP_Block class that represents the block being rendered. 14 | * 15 | * @package create-wordpress-plugin 16 | */ 17 | 18 | $create_wordpress_plugin_post_id = is_numeric( $block->context['postId'] ?? null ) ? (int) $block->context['postId'] : 0; 19 | if ( empty( $create_wordpress_plugin_post_id ) ) { 20 | return; 21 | } 22 | $subheadline = get_post_meta( $create_wordpress_plugin_post_id, 'create_wordpress_plugin_subheadline', true ); 23 | 24 | if ( ! is_string( $subheadline ) || empty( $subheadline ) ) { 25 | return; 26 | } 27 | ?> 28 |

> 29 | 30 |

31 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # Packages 2 | 3 | The packages folder should contain workspaces for shared code used by a plugin, theme, or another package. 4 | 5 | The directories in this folder should contain a `package.json` file in order to be recognized in the NPM monorepo with Turborepo. 6 | 7 | ## Generating a new package 8 | 9 | > Code splitting the WordPress project into individual workspaces is a great way to organize your code, speed up tasks, and improve the local development experience. With Turborepo's code generation, it's easy to generate new source code for packages, modules, and even individual UI components in a structured way that integrates with the rest of your repository. 10 | 11 | -- https://turbo.build/repo/docs/core-concepts/monorepos/code-generation 12 | 13 | ### Generate a new workspace package. 14 | 15 | Add a new, empty package to you the workspace. 16 | 17 | A script is provided in the root `package.json` file that will generate a new workspace package in the `packages` folder. You will be prompted to enter the name of the new package. 18 | 19 | ``` 20 | npm run create-package 21 | ``` 22 | A package directory will be created in the `packages` folder with the following base structure: 23 | 24 | ``` 25 | ├── packages 26 | │ └── package-name 27 | │ ├── package.json 28 | │ └── README.md 29 | ``` 30 | 31 | All packages created in the package folder will be automatically added to the `package.json` workspaces array in the root of the repository. 32 | -------------------------------------------------------------------------------- /plugin-templates/entries/slotfills/subheadline/Subheadline.tsx: -------------------------------------------------------------------------------- 1 | /* global tinyMCEPreInit */ 2 | import { PluginDocumentSettingPanel } from '@wordpress/editor'; 3 | import { __ } from '@wordpress/i18n'; 4 | import { Editor } from '@tinymce/tinymce-react'; 5 | import { usePostMetaValue } from '@alleyinteractive/block-editor-tools'; 6 | 7 | declare global { 8 | const tinyMCEPreInit: { 9 | baseURL: string, 10 | }; 11 | } 12 | 13 | function Subheadline() { 14 | const [subheadline, setSubheadline] = usePostMetaValue('create_wordpress_plugin_subheadline'); 15 | if (!tinyMCEPreInit.baseURL) { 16 | return null; 17 | } 18 | return ( 19 | 23 | {/* @ts-ignore - types are not available for Editor */} 24 | 38 | 39 | ); 40 | } 41 | 42 | export default Subheadline; 43 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search-facets/edit.tsx: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | import { useBlockProps } from '@wordpress/block-editor'; 3 | import { Disabled } from '@wordpress/components'; 4 | 5 | import './index.scss'; 6 | 7 | /** 8 | * The create-wordpress-plugin/theme-faceted-search-facets block edit function. 9 | * 10 | * @return {WPElement} Element to render. 11 | */ 12 | export default function Edit() { 13 | return ( 14 |
15 |

16 | {__('Filter By', 'create-wordpress-plugin')} 17 |

18 | 19 |
20 | {__('Facet Title', 'create-wordpress-plugin')} 21 | 25 | 29 |
30 | 31 | {__('Reset', 'create-wordpress-plugin')} 32 | 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /plugin-templates/src/features/class-search-customizations.php: -------------------------------------------------------------------------------- 1 | enable_empty_search() 44 | ->enable_post_type_aggregation() 45 | ->restrict_post_types( $this->post_types ); 46 | 47 | foreach ( $this->taxonomies as $taxonomy ) { 48 | $es_config->enable_taxonomy_aggregation( $taxonomy ); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /plugin-templates/src/features/class-subheadline.php: -------------------------------------------------------------------------------- 1 | 'wp_kses_post', 51 | 'single' => true, 52 | 'type' => 'string', 53 | 'show_in_rest' => true, 54 | ] 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ci-templates/.github/workflows/deploy-to-vip-built-branch.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to WordPress VIP -built Branch 2 | 3 | on: 4 | push: 5 | branches: 6 | - production 7 | - preprod 8 | - develop 9 | 10 | jobs: 11 | build-and-sync: 12 | if: github.repository == 'wpcomvip/VIP_REPO_SLUG' 13 | runs-on: ubuntu-latest 14 | name: "Build and Deploy on VIP" 15 | timeout-minutes: 10 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Install composer dependencies 21 | uses: alleyinteractive/action-test-php@v1 22 | with: 23 | install-command: 'composer install --no-dev --prefer-dist --no-interaction --no-progress' 24 | php-version: '8.3' 25 | skip-audit: 'true' 26 | skip-services: 'true' 27 | skip-test: 'true' 28 | skip-wordpress-install: 'true' 29 | 30 | - name: Build 31 | uses: alleyinteractive/action-test-node@v1 32 | with: 33 | node-version: '22' 34 | skip-audit: 'true' 35 | skip-test: 'true' 36 | 37 | - name: Deploy to -built Branch 38 | uses: alleyinteractive/action-deploy-to-remote-repository@v3 39 | with: 40 | remote_repo: 'git@github.com:wpcomvip/VIP_REPO_SLUG.git' 41 | remote_branch: ${{ github.ref_name }}-built 42 | remote_branch_is_orphan: 'true' 43 | exclude_list: '.git, .turbo, node_modules' 44 | ssh-key: ${{ secrets.REMOTE_REPO_SSH_KEY }} 45 | 46 | - name: Notify Slack 47 | if: always() 48 | uses: alleyinteractive/action-notify-slack@develop 49 | with: 50 | status: ${{ job.status }} 51 | webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} 52 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-primary-term/render.php: -------------------------------------------------------------------------------- 1 | $attributes 11 | * 12 | * @var array $attributes The array of attributes for this block. 13 | * @var string $content Rendered block output. ie. . 14 | * @var WP_Block $block The instance of the WP_Block class that represents the block being rendered. 15 | * 16 | * @package create-wordpress-plugin 17 | */ 18 | 19 | $post_id = $block->context['postId'] ?? get_the_ID(); 20 | $post_id = is_numeric( $post_id ) ? (int) $post_id : 0; 21 | $primary_term_rest = new \Create_WordPress_Plugin\Features\Primary_Term_Rest(); 22 | 23 | if ( ! isset( $attributes['taxonomy'] ) || ! is_string( $attributes['taxonomy'] ) ) { 24 | return; 25 | } 26 | 27 | $primary_term = $primary_term_rest->get_primary_term( $post_id, $attributes['taxonomy'] ); 28 | if ( ! $primary_term ) { 29 | return; 30 | } 31 | ?> 32 | > 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /ci-templates/.github/workflows/deploy-to-pantheon.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Pantheon 2 | 3 | on: 4 | push: 5 | branches: 6 | - production 7 | - preprod 8 | - develop 9 | 10 | jobs: 11 | build-and-sync: 12 | runs-on: ubuntu-latest 13 | name: "Build and Deploy to Pantheon" 14 | timeout-minutes: 10 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Install composer dependencies 20 | uses: alleyinteractive/action-test-php@v1 21 | with: 22 | install-command: 'composer install --no-dev --prefer-dist --no-interaction --no-progress' 23 | php-version: '8.3' 24 | skip-audit: 'true' 25 | skip-services: 'true' 26 | skip-test: 'true' 27 | skip-wordpress-install: 'true' 28 | 29 | - name: Build 30 | uses: alleyinteractive/action-test-node@v1 31 | with: 32 | node-version: '22' 33 | skip-audit: 'true' 34 | skip-test: 'true' 35 | 36 | - name: Deploy to Pantheon 37 | uses: alleyinteractive/action-deploy-to-pantheon@v2 38 | with: 39 | base_directory: './' 40 | destination_directory: 'wp-content/' 41 | exclude_list: '.git, .github, .gitmodules, .pantheon, node_modules' 42 | pantheon_env_name: ${{ github.ref_name }} 43 | pantheon_machine_token: ${{ secrets.PANTHEON_MACHINE_TOKEN }} 44 | pantheon_site_id: ${{ secrets.PANTHEON_SITE_ID }} 45 | pantheon_site: ${{ secrets.PANTHEON_SITE }} 46 | ssh-key: ${{ secrets.REMOTE_REPO_SSH_KEY }} 47 | autopromote: 'true' 48 | 49 | - name: Notify Slack 50 | if: always() 51 | uses: alleyinteractive/action-notify-slack@develop 52 | with: 53 | status: ${{ job.status }} 54 | webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} 55 | -------------------------------------------------------------------------------- /.phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | PHP_CodeSniffer standard for create-wordpress-project. 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | build/ 30 | tests/ 31 | vendor/ 32 | 33 | 34 | 35 | 36 | 37 | 38 | ./plugins/create-wordpress-plugin/src/post-types/ 39 | 40 | 41 | ./plugins/create-wordpress-plugin/src/post-types/ 42 | 43 | 44 | ./plugin-templates/ 45 | 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Create WordPress Project 3 | 4 | This is a skeleton WordPress project that can be used to quickly scaffold 5 | WordPress projects that work with Alley's 6 | [create-wordpress-plugin](https://github.com/alleyinteractive/create-wordpress-plugin) 7 | and 8 | [create-wordpress-theme](https://github.com/alleyinteractive/create-wordpress-theme) 9 | starter kits. 10 | 11 | This template is rooted at the `wp-content` and supports both Pantheon and 12 | WordPress VIP hosting environments with deployment workflows for each. It is 13 | powered by [Composer](https://getcomposer.org/) to manage dependencies and 14 | [Turborepo](https://turbo.build/repo) to build front-end assets in your project. The template includes both GitHub Actions and Buddy workflows for continuous integration and deployment. 15 | 16 | Also included is a configuration script to easily replace all placeholders throughout the project and scaffold out your plugin/theme. 17 | 18 | ## Getting Started 19 | 20 | Follow these steps to get started: 21 | 22 | 1. Press the "Use template" button at the top of this repo to create a new repo 23 | with the contents of this skeleton. 24 | 2. Run `make` (or `php ./configure.php`) to run the configuration script that 25 | will replace all placeholders throughout all the files. 26 | 3. Have fun creating your WordPress site! 🎊 27 | 28 | 29 | 30 | # create-wordpress-project 31 | 32 | A skeleton for WordPress projects rooted at `wp-content` using Alley's best 33 | practices for WordPress development. 34 | 35 | ## Credits 36 | 37 | This project is actively maintained by [Alley 38 | Interactive](https://github.com/alleyinteractive). Like what you see? [Come work 39 | with us](https://alley.com/careers/). 40 | 41 | - [author_name](https://github.com/author_username) 42 | - [Alley Interactive](https://github.com/alleyinteractive) 43 | - [All Contributors](../../contributors) 44 | 45 | ## License 46 | 47 | The GNU General Public License (GPL) license. Please see [License File](LICENSE) for more information. 48 | -------------------------------------------------------------------------------- /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: General Tests 2 | 3 | # Tests to be performed on the create-wordpress-project template repository. 4 | # This workflow will be deleted upon configuration. 5 | on: 6 | # Check on PR to develop branch. 7 | pull_request: 8 | branches: 9 | - production 10 | types: [opened, synchronize, reopened, ready_for_review] 11 | # Also check on weekly schedule to catch regressions early. 12 | schedule: 13 | - cron: '0 0 * * 0' 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | general-tests: 21 | if: github.event.pull_request.draft == false 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 10 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Run General Tests 29 | # See https://github.com/alleyinteractive/action-test-general for more options 30 | uses: alleyinteractive/action-test-general@develop 31 | 32 | - name: Composer install and make 33 | uses: alleyinteractive/action-test-php@develop 34 | with: 35 | php-version: '8.3' 36 | github-token: ${{ secrets.COMPOSER_ACCESS_TOKEN}} 37 | install-command: | 38 | composer install 39 | (yes '' || true) | php ./configure.php --project_name="Create WordPress Project" --author_name=Testing --author_email=testing@alley.com 40 | skip-test: true 41 | 42 | - name: Run Node Tests 43 | uses: alleyinteractive/action-test-node@develop 44 | with: 45 | install-command: npm install 46 | 47 | - name: Run PHP Tests 48 | uses: alleyinteractive/action-test-php@develop 49 | with: 50 | php-version: '8.3' 51 | github-token: ${{ secrets.COMPOSER_ACCESS_TOKEN}} 52 | skip-install: true 53 | test-command: | 54 | composer validate --strict 55 | # Disabled because phpcs is flagging Alley\WP as a namespace and phpstan fails 56 | # composer test 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaces": [ 3 | "packages/*", 4 | "plugins/create-wordpress-plugin", 5 | "themes/create-wordpress-theme" 6 | ], 7 | "dependencies": { 8 | "@alleyinteractive/block-editor-tools": "^0.11.0", 9 | "@tinymce/tinymce-react": "^5.1.1", 10 | "@wordpress/block-editor": "^14.3.15" 11 | }, 12 | "devDependencies": { 13 | "@alleyinteractive/build-tool": "^0.1.5", 14 | "@alleyinteractive/create-entry": "^0.0.4", 15 | "@alleyinteractive/eslint-config": "^0.1.6", 16 | "@alleyinteractive/stylelint-config": "^0.0.2", 17 | "@alleyinteractive/tsconfig": "^0.1.1", 18 | "@babel/preset-env": "^7.26.0", 19 | "@testing-library/jest-dom": "^6.6.3", 20 | "@testing-library/react": "^16.1.0", 21 | "@types/jest": "^29.5.14", 22 | "@types/wordpress__block-editor": "^11.5.16", 23 | "@types/wordpress__blocks": "^12.5.17", 24 | "@types/wordpress__edit-post": "^8.4.2", 25 | "@wordpress/babel-preset-default": "^8.8.2", 26 | "@wordpress/scripts": "^30.0.6", 27 | "babel-jest": "^29.7.0", 28 | "eslint-import-resolver-typescript": "^3.7.0", 29 | "eslint-plugin-import": "^2.31.0", 30 | "jest": "^29.7.0", 31 | "stylelint-config-sass-guidelines": "^12.1.0", 32 | "ts-jest": "^29.2.5", 33 | "turbo": "^2.4.4", 34 | "typescript": "^5.7.2", 35 | "webpack-cli": "^5.1.4", 36 | "wp-types": "^4.67.0" 37 | }, 38 | "engines": { 39 | "node": "22", 40 | "npm": "10" 41 | }, 42 | "license": "GPL-2.0-or-later", 43 | "name": "create-wordpress-project", 44 | "scripts": { 45 | "build": "turbo run build", 46 | "create-package": "turbo gen workspace --destination packages --type package", 47 | "lint": "turbo run lint", 48 | "lint:fix": "turbo run lint:fix", 49 | "packages-update": "turbo run packages-update -- --dist-tag=wp-6.7", 50 | "start:hot": "turbo run start:hot", 51 | "start": "turbo run start", 52 | "stylelint": "turbo run stylelint", 53 | "stylelint:fix": "turbo run stylelint:fix", 54 | "test:watch": "turbo run test:watch", 55 | "test": "turbo run test" 56 | }, 57 | "version": "1.0.0", 58 | "packageManager": "npm@10.9.2" 59 | } 60 | -------------------------------------------------------------------------------- /ci-templates/.github/workflows/all-pr-tests.yml: -------------------------------------------------------------------------------- 1 | name: "All Pull Request Tests" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - production 7 | types: [opened, synchronize, reopened, ready_for_review] 8 | 9 | jobs: 10 | # We use a single job to ensure that all steps 11 | # run in the same environment and reduce the number of minutes used. 12 | pr-tests: 13 | # Don't run on draft PRs 14 | if: github.event.pull_request.draft == false && github.repository == 'alleyinteractive/create-wordpress-project' 15 | runs-on: ubuntu-latest 16 | # Timeout after 10 minutes 17 | timeout-minutes: 10 18 | # Cancel any existing runs of this workflow 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} 21 | cancel-in-progress: true 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Run General Tests 27 | # See https://github.com/alleyinteractive/action-test-general for more options 28 | uses: alleyinteractive/action-test-general@v1 29 | 30 | - name: Run Node Tests 31 | # See https://github.com/alleyinteractive/action-test-node for more options 32 | uses: alleyinteractive/action-test-node@v1 33 | 34 | - name: Run PHP Tests 35 | # See https://github.com/alleyinteractive/action-test-php for more options 36 | uses: alleyinteractive/action-test-php@v1 37 | with: 38 | php-version: '8.3' 39 | # This required job ensures that all PR checks have passed before merging. 40 | all-pr-checks-passed: 41 | name: All PR checks passed 42 | needs: 43 | - pr-tests 44 | runs-on: ubuntu-latest 45 | if: always() 46 | permissions: 47 | pull-requests: read 48 | statuses: write 49 | contents: read 50 | steps: 51 | - name: Check job statuses 52 | run: | 53 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 54 | echo "One or more jobs failed" 55 | exit 1 56 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 57 | echo "One or more jobs were cancelled" 58 | exit 1 59 | else 60 | echo "All jobs passed or were skipped" 61 | exit 0 62 | fi 63 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search-facets/render.php: -------------------------------------------------------------------------------- 1 | $attributes 10 | * 11 | * @var array $attributes The array of attributes for this block. 12 | * @var string $content Rendered block output. ie. . 13 | * @var WP_Block $block The instance of the WP_Block class that represents the block being rendered. 14 | * 15 | * @package create-wordpress-plugin 16 | */ 17 | 18 | if ( ! function_exists( 'elasticsearch_extensions' ) ) { 19 | return; 20 | } 21 | 22 | // Get the aggregations that we need to work with manually. 23 | $post_type_aggregation = elasticsearch_extensions()->get_aggregation_by_query_var( 'post_type' ); 24 | $category_aggregation = elasticsearch_extensions()->get_aggregation_by_query_var( 'taxonomy_category' ); 25 | 26 | // If we failed to get aggregations, don't render anything. 27 | if ( is_null( $post_type_aggregation ) && is_null( $category_aggregation ) ) { 28 | return; 29 | } 30 | 31 | // Negotiate whether any of the facet fields have been set. 32 | $is_facet_set = ( 33 | ( $post_type_aggregation && $post_type_aggregation->get_query_values() ) 34 | || ( $category_aggregation && $category_aggregation->get_query_values() ) 35 | ); 36 | 37 | ?> 38 |
> 39 |

40 | 41 |

42 | 43 | checkboxes(); 46 | endif; 47 | ?> 48 | 49 | checkboxes(); 52 | endif; 53 | ?> 54 | 55 | 56 | 57 | 58 |
59 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-faceted-search/edit.tsx: -------------------------------------------------------------------------------- 1 | import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; 2 | import { __ } from '@wordpress/i18n'; 3 | 4 | import './index.scss'; 5 | 6 | /** 7 | * The create-wordpress-plugin/theme-faceted-search block edit function. 8 | * 9 | * @return {WPElement} Element to render. 10 | */ 11 | export default function Edit() { 12 | const blockProps = useBlockProps(); 13 | const SEARCH_RESULTS_TEMPLATE = [ 14 | ['core/heading', { level: 1, textAlign: 'center', content: __('Search', 'create-wordpress-plugin') }], 15 | ['core/search', { 16 | label: __('Search', 'create-wordpress-plugin'), 17 | showLabel: false, 18 | placeholder: __('Search', 'create-wordpress-plugin'), 19 | buttonText: __('Search', 'create-wordpress-plugin'), 20 | }], 21 | ['core/separator', { align: 'wide' }], 22 | ['core/columns', { align: 'wide' }, [ 23 | ['core/column', { width: '33.33%' }, [ 24 | ['create-wordpress-plugin/theme-faceted-search-facets', {}], 25 | ]], 26 | ['core/column', { width: '66.66%' }, [ 27 | ['create-wordpress-plugin/theme-search-meta', {}], 28 | ['core/query', { 29 | query: { 30 | pages: 0, 31 | offset: 0, 32 | postType: 'post', 33 | order: 'desc', 34 | orderBy: 'date', 35 | inherit: true, 36 | }, 37 | }, [ 38 | ['core/post-template', {}, [ 39 | ['core/columns', {}, [ 40 | ['core/column', { width: '' }, [ 41 | ['core/post-title', { isLink: true }], 42 | ['core/post-date'], 43 | ['core/post-excerpt'], 44 | ]], 45 | ['core/column', { width: '150px' }, [ 46 | ['core/post-featured-image', { isLink: true, width: '150px' }], 47 | ]], 48 | ]], 49 | ]], 50 | ['core/query-pagination', { paginationArrow: 'chevron', showLabel: false }, [ 51 | ['core/query-pagination-previous', {}], 52 | ['core/query-pagination-numbers', {}], 53 | ['core/query-pagination-next', {}], 54 | ]], 55 | ]], 56 | ]], 57 | ]], 58 | ]; 59 | 60 | return ( 61 |
62 | {/* @ts-ignore */ } 63 | 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-primary-term/edit.tsx: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; 3 | import { usePostById } from '@alleyinteractive/block-editor-tools'; 4 | import { WP_REST_API_Post } from 'wp-types'; // eslint-disable-line camelcase 5 | import { ToggleControl, PanelRow, PanelBody } from '@wordpress/components'; 6 | 7 | type EditProps = { 8 | attributes: { 9 | isLink: boolean; 10 | taxonomy: string; 11 | }; 12 | context: { 13 | postId?: number; 14 | }; 15 | setAttributes: (newAttributes: Partial) => void; 16 | }; 17 | 18 | type PostWithPrimaryTerm = WP_REST_API_Post & { // eslint-disable-line camelcase 19 | create_wordpress_plugin_primary_term: { 20 | [taxonomy: string]: { 21 | term_id: number; 22 | term_name: string; 23 | term_link: string; 24 | }; 25 | }; 26 | }; 27 | 28 | /** 29 | * The create-wordpress-plugin/theme-primary-term block edit function. 30 | * 31 | * @return {WPElement} Element to render. 32 | */ 33 | export default function Edit({ 34 | attributes: { 35 | isLink = true, 36 | taxonomy = 'category', 37 | }, 38 | context: { 39 | postId, 40 | }, 41 | setAttributes, 42 | }: EditProps) { 43 | const record = usePostById(postId); 44 | 45 | let primaryTerm = null; 46 | if (record) { 47 | const primaryTermField = (record as PostWithPrimaryTerm).create_wordpress_plugin_primary_term; 48 | primaryTerm = primaryTermField[taxonomy]; 49 | } else { 50 | primaryTerm = { 51 | term_name: __('Primary Term', 'wp-newsletter-builder'), 52 | term_id: null, 53 | term_link: '', 54 | }; 55 | } 56 | 57 | return ( 58 | <> 59 |
60 | {isLink ? ( 61 | {/* eslint-disable-line */} 62 | {primaryTerm.term_name} 63 | 64 | ) : primaryTerm.term_name} 65 |
66 | 67 | 68 | 69 | setAttributes({ isLink: next })} 73 | /> 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /plugin-templates/entries/slotfills/index.php: -------------------------------------------------------------------------------- 1 | is_scalar( $item ) ? (string) $item : '', $asset_file['dependencies'] ) 49 | : []; 50 | 51 | // Validate and sanitize version. 52 | $version = is_string( $asset_file['version'] ) || is_numeric( $asset_file['version'] ) 53 | ? (string) $asset_file['version'] 54 | : null; 55 | 56 | wp_register_script( 57 | 'create-wordpress-plugin_slotfills', 58 | plugins_url( 'index.js', __FILE__ ), 59 | $dependencies, 60 | $version, 61 | true 62 | ); 63 | 64 | wp_set_script_translations( 'create-wordpress-plugin_slotfills', 'create-wordpress-plugin' ); 65 | } 66 | 67 | /** 68 | * Enqueue block editor assets for this slotfill. 69 | */ 70 | function action_enqueue_slotfills_assets(): void { 71 | wp_enqueue_script( 'create-wordpress-plugin_slotfills' ); 72 | } 73 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-search-meta/render.php: -------------------------------------------------------------------------------- 1 | $attributes 10 | * 11 | * @var array $attributes The array of attributes for this block. 12 | * @var string $content Rendered block output. ie. . 13 | * @var WP_Block $block The instance of the WP_Block class that represents the block being rendered. 14 | * 15 | * @package create-wordpress-plugin 16 | */ 17 | 18 | global $wp_query; // @phpstan-ignore-line 19 | 20 | $search_query = get_search_query( false ); 21 | $found_posts = $wp_query instanceof WP_Query ? $wp_query->found_posts : 0; 22 | $found_posts_formatted = 10000 === $found_posts ? '10,000+' : number_format( $found_posts ); 23 | 24 | ?> 25 |
> 26 | 27 | 28 | 37 | 38 | 39 | 40 | 70 | 71 | 72 |
73 | -------------------------------------------------------------------------------- /mu-plugins/001-composer.php: -------------------------------------------------------------------------------- 1 | composer install locally in the wp-content folder for this project.' 20 | ); 21 | } 22 | 23 | /** 24 | * Composer Configuration 25 | * 26 | * @var array{ 27 | * config?: array{ 28 | * platform?: array{ 29 | * php?: string, 30 | * } 31 | * } 32 | * } $composer Composer configuration from composer.json, which may specify the platform PHP version. 33 | */ 34 | $composer = json_decode( file_get_contents( dirname( __DIR__ ) . '/composer.json' ) ?: '', true ); 35 | $required_version = $composer['config']['platform']['php'] ?? '8.0.0'; 36 | 37 | // Display a friendly error message if the wrong PHP version is used locally. 38 | if ( version_compare( PHP_VERSION, $required_version, '<' ) ) { 39 | wp_die( 40 | sprintf( 41 | 'This site requires a PHP version >= %s. You are running %s. Please switch to %s on your machine.', 42 | esc_html( $required_version ), 43 | PHP_VERSION, 44 | esc_html( $required_version ), 45 | ), 46 | ); 47 | } 48 | 49 | // Unset all variables to prevent any external usage. 50 | unset( $composer, $required_version ); 51 | } 52 | 53 | // Load the Composer autoloader or add a notice if it doesn't exist. 54 | if ( file_exists( $composer_autoloader_path ) ) { 55 | require_once $composer_autoloader_path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable 56 | } else { 57 | // Include an error notice. 58 | add_action( 59 | 'admin_notices', 60 | function () { 61 | if ( 'local' === wp_get_environment_type() ) { 62 | printf( 63 | '

%s

', 64 | esc_html( 'Composer needs to be installed for the site.' ) 65 | ); 66 | } else { 67 | printf( 68 | '

%s

', 69 | esc_html( 'Composer is not installed on the site! Please contact the site administrator.' ) 70 | ); 71 | } 72 | } 73 | ); 74 | } 75 | 76 | // Don't pollute global scope. 77 | unset( $composer_autoloader_path ); 78 | -------------------------------------------------------------------------------- /plugin-templates/src/features/class-featured-image-caption.php: -------------------------------------------------------------------------------- 1 | $block 33 | * 34 | * @param string $block_content The existing block content. 35 | * @param array $block The full block, including name and attributes. 36 | * @param WP_Block $instance The block instance. 37 | * @return string Modified block content. 38 | */ 39 | public function add_caption_to_featured_image( string $block_content, array $block, WP_Block $instance ): string { 40 | $post_id = is_numeric( $instance->context['postId'] ?? null ) ? (int) $instance->context['postId'] : 0; 41 | if ( empty( $post_id ) ) { 42 | return $block_content; 43 | } 44 | $featured_image_caption = get_post_meta( $post_id, 'create_wordpress_plugin_featured_image_caption', true ); 45 | if ( empty( $featured_image_caption ) ) { 46 | $featured_image_id = get_post_meta( $post_id, '_thumbnail_id', true ); 47 | $featured_image_caption = wp_get_attachment_caption( is_numeric( $featured_image_id ) ? (int) $featured_image_id : 0 ); 48 | } 49 | 50 | if ( ! empty( $featured_image_caption ) ) { 51 | $block_content = str_replace( '', '
' . esc_html( is_string( $featured_image_caption ) ? $featured_image_caption : '' ) . '
', $block_content ); 52 | } 53 | 54 | return $block_content; 55 | } 56 | 57 | /** 58 | * Registers the meta field only for post types that support featured images. 59 | */ 60 | public function add_meta_field(): void { 61 | register_meta_helper( 62 | 'post', 63 | get_post_types_by_support( 'thumbnail' ), 64 | 'create_wordpress_plugin_featured_image_caption', 65 | [ 66 | 'sanitize_callback' => 'sanitize_text_field', 67 | 'single' => true, 68 | 'type' => 'string', 69 | 'show_in_rest' => true, 70 | ] 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alleyinteractive/create-wordpress-project", 3 | "description": "A skeleton WordPress project", 4 | "license": "GPL-2.0-or-later", 5 | "type": "project", 6 | "keywords": [ 7 | "alleyinteractive", 8 | "create-wordpress-project" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Alley", 13 | "email": "info@alley.com" 14 | } 15 | ], 16 | "homepage": "https://github.com/alleyinteractive/create-wordpress-project", 17 | "require": { 18 | "php": "^8.3", 19 | "alleyinteractive/composer-wordpress-autoloader": "^1.0", 20 | "alleyinteractive/wp-plugin-loader": "^0.1.3", 21 | "composer/installers": "^1.0", 22 | "pantheon-systems/pantheon-mu-plugin": "^1.0" 23 | }, 24 | "require-dev": { 25 | "alleyinteractive/alley-coding-standards": "^2.0", 26 | "mantle-framework/testkit": "^1.4", 27 | "szepeviktor/phpstan-wordpress": "^2.0", 28 | "wpackagist-plugin/create-block-theme": "^2.6" 29 | }, 30 | "repositories": [ 31 | { 32 | "type": "composer", 33 | "url": "https://wpackagist.org", 34 | "only": [ 35 | "wpackagist-plugin/*" 36 | ] 37 | } 38 | ], 39 | "minimum-stability": "dev", 40 | "prefer-stable": true, 41 | "config": { 42 | "allow-plugins": { 43 | "alleyinteractive/composer-wordpress-autoloader": true, 44 | "composer/installers": true, 45 | "dealerdirect/phpcodesniffer-composer-installer": true, 46 | "pestphp/pest-plugin": true 47 | }, 48 | "sort-packages": true, 49 | "vendor-dir": "mu-plugins/vendor" 50 | }, 51 | "extra": { 52 | "installer-paths": { 53 | "mu-plugins/{$name}": [ 54 | "type:wordpress-muplugin" 55 | ], 56 | "plugins/{$name}": [ 57 | "type:wordpress-plugin" 58 | ] 59 | }, 60 | "wordpress-autoloader": { 61 | "autoload": { 62 | "Create_WordPress_Plugin\\": "plugins/create-wordpress-plugin/src" 63 | }, 64 | "autoload-dev": { 65 | "Create_WordPress_Plugin\\Tests\\": "plugins/create-wordpress-plugin/tests" 66 | } 67 | } 68 | }, 69 | "scripts": { 70 | "phpcbf": [ 71 | "@phpcbf:plugin", 72 | "@phpcbf:theme" 73 | ], 74 | "phpcbf:plugin": "cd plugins/create-wordpress-plugin && phpcbf .", 75 | "phpcbf:theme": "cd themes/create-wordpress-theme && phpcbf .", 76 | "phpcs": [ 77 | "@phpcs:plugin", 78 | "@phpcs:theme" 79 | ], 80 | "phpcs:plugin": "cd plugins/create-wordpress-plugin && phpcs .", 81 | "phpcs:theme": "cd themes/create-wordpress-theme && phpcs .", 82 | "phpstan": "phpstan --memory-limit=1G", 83 | "phpunit": [ 84 | "@phpunit:plugin" 85 | ], 86 | "phpunit:plugin": "cd plugins/create-wordpress-plugin && phpunit", 87 | "setup": [ 88 | "composer install --quiet --no-interaction" 89 | ], 90 | "test": [ 91 | "@phpcs", 92 | "@phpstan", 93 | "@phpunit" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /plugin-templates/src/features/class-msm-sitemap-integration.php: -------------------------------------------------------------------------------- 1 | $this->post_types ); 32 | 33 | // Turn off other sitemap providers... 34 | 35 | // Core. 36 | add_filter( 'wp_sitemaps_enabled', '__return_false' ); 37 | add_action( 'wp_sitemaps_init', [ $this, 'action_wp_sitemaps_init' ] ); 38 | // WordPress SEO, by forcing the option to be false. 39 | add_filter( 40 | 'option_wpseo', 41 | function ( $value ) { 42 | if ( is_array( $value ) ) { 43 | $value['enable_xml_sitemap'] = false; 44 | } 45 | 46 | return $value; 47 | } 48 | ); 49 | // Jetpack, by removing sitemaps as an available module and removing it from active modules. 50 | add_filter( 51 | 'jetpack_get_available_modules', 52 | function ( $modules ) { 53 | if ( is_array( $modules ) ) { 54 | unset( $modules['sitemaps'] ); 55 | } 56 | 57 | return $modules; 58 | } 59 | ); 60 | add_filter( 61 | 'jetpack_active_modules', 62 | fn ( $active ) => array_values( array_diff( (array) $active, [ 'sitemaps' ] ) ) 63 | ); 64 | } 65 | 66 | /** 67 | * Fires when initializing the Sitemaps object. 68 | * 69 | * @param WP_Sitemaps $wp_sitemaps Sitemaps object. 70 | */ 71 | public function action_wp_sitemaps_init( $wp_sitemaps ): void { 72 | /* 73 | * By default, core will continue to register its rewrite rules for sitemaps even when core 74 | * sitemaps are disabled so that it can send a 404 response when attempting to access 75 | * sitemaps at their default URLs. This makes sense in most cases, except that both core and 76 | * MSM Sitemap use the 'sitemap' query var to render their sitemaps. We don't want a 404 77 | * response to be sent when accessing the MSM sitemap, so we remove the action that sends 78 | * the 404 when the 'sitemap' query var is set to 'true', which is the only value that MSM 79 | * Sitemap uses for the 'sitemap' query var in a rewrite rule. 80 | */ 81 | add_action( 82 | 'template_redirect', 83 | function () use ( $wp_sitemaps ) { 84 | $qv = get_query_var( 'sitemap' ); 85 | 86 | if ( 'true' === $qv ) { 87 | remove_action( 'template_redirect', [ $wp_sitemaps, 'render_sitemaps' ] ); 88 | } 89 | }, 90 | 0 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /plugin-templates/blocks/theme-navigation/edit.tsx: -------------------------------------------------------------------------------- 1 | import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; 2 | import { 3 | PanelBody, 4 | Placeholder, 5 | SelectControl, 6 | Spinner, 7 | } from '@wordpress/components'; 8 | import { useSelect } from '@wordpress/data'; 9 | import { menu } from '@wordpress/icons'; 10 | import { __ } from '@wordpress/i18n'; 11 | 12 | /** 13 | * A global type should be declared for this package. This is a temporary workaround until we 14 | * decide on where global types should be declared. 15 | */ 16 | // @ts-ignore 17 | import ServerSideRender from '@wordpress/server-side-render'; 18 | 19 | import './index.scss'; 20 | 21 | interface MenuLocation { 22 | name: string; 23 | description: string; 24 | } 25 | 26 | export interface SelectOptions { 27 | label: string; 28 | value: string; 29 | } 30 | 31 | /** 32 | * Given an array of menu locations, return an array of objects that can be used for 33 | * SelectControl components. 34 | * 35 | * @param menuLocations Registered menu locations. 36 | */ 37 | function getMenuLocationsSelectOptions(menuLocations: MenuLocation[]): SelectOptions[] { 38 | const menuLocationsList = []; 39 | 40 | menuLocationsList.push({ label: __('Select a menu location', 'create-wordpress-plugin'), value: '' }); 41 | 42 | menuLocations 43 | .forEach((location) => { 44 | const { description, name } = location; 45 | menuLocationsList.push({ label: description, value: name }); 46 | }); 47 | 48 | return menuLocationsList; 49 | } 50 | 51 | interface Attributes { 52 | menuLocation: string; 53 | } 54 | 55 | interface EditProps { 56 | attributes: Attributes; 57 | setAttributes: (next: Partial) => void; 58 | } 59 | 60 | export default function Edit({ 61 | attributes: { 62 | menuLocation, 63 | }, 64 | setAttributes, 65 | }: EditProps) { 66 | const blockProps = useBlockProps(); 67 | 68 | // @ts-ignore - useSelect doesn't export proper types. 69 | const menuLocations = useSelect((select) => select('core').getMenuLocations('root', 'menu'), []); 70 | 71 | // Loading menu locations. 72 | if (!Array.isArray(menuLocations)) { 73 | return ( 74 |
75 | 76 |
77 | ); 78 | } 79 | 80 | // No menu locations have been registered. 81 | if (menuLocations.length === 0) { 82 | return ( 83 |
84 | 89 |
90 | ); 91 | } 92 | 93 | return ( 94 |
95 | { 96 | menuLocation ? ( 97 | 98 | ) : ( 99 | 104 | ) 105 | } 106 | 107 | 108 | 112 | setAttributes({ menuLocation: next })} 114 | value={menuLocation || ''} 115 | options={getMenuLocationsSelectOptions(menuLocations)} 116 | /> 117 | 118 | 119 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /plugin-templates/src/features/class-primary-term-rest.php: -------------------------------------------------------------------------------- 1 | [ $this, 'rest_callback' ], 44 | 'update_callback' => null, 45 | 'schema' => [ 46 | 'description' => __( 'The primary term for the post.', 'create-wordpress-plugin' ), 47 | 'type' => [ 48 | 'term_name' => 'string', 49 | 'term_id' => 'integer', 50 | 'term_link' => 'string', 51 | ], 52 | ], 53 | ] 54 | ); 55 | } 56 | 57 | /** 58 | * Callback function for the REST field. 59 | * 60 | * @param array $request The REST request. 61 | * 62 | * @phpstan-param array{id: int} $request 63 | * 64 | * @return array An array containing taxonomy slugs as keys with term objects as values. 65 | * 66 | * @phpstan-return array 67 | */ 68 | public function rest_callback( array $request ): array { 69 | $output = []; 70 | $post_id = $request['id']; 71 | foreach ( $this->taxonomies as $taxonomy ) { 72 | $output[ $taxonomy ] = $this->get_primary_term( $post_id, $taxonomy ); 73 | } 74 | return $output; 75 | } 76 | 77 | /** 78 | * Gets the primary term for the post for a given taxonomy. 79 | * 80 | * @param int $post_id The post id. 81 | * @param string $taxonomy The taxonomy to get the primary term for. 82 | * 83 | * @return array An array containing the term name, ID, and link. 84 | * 85 | * @phpstan-return array{term_name?: string, term_id?: int, term_link?: string} 86 | */ 87 | public function get_primary_term( int $post_id, string $taxonomy ): array { 88 | $term = []; 89 | if ( function_exists( 'yoast_get_primary_term_id' ) ) { 90 | $primary_term_id = yoast_get_primary_term_id( $taxonomy, $post_id ); 91 | } 92 | if ( empty( $primary_term_id ) ) { 93 | $terms = get_the_terms( $post_id, $taxonomy ); 94 | if ( is_array( $terms ) && ! empty( $terms[0]->term_id ) ) { 95 | $primary_term_id = $terms[0]->term_id; 96 | } 97 | } 98 | if ( ! empty( $primary_term_id ) && is_int( $primary_term_id ) ) { 99 | $primary_term = get_term( $primary_term_id, $taxonomy ); 100 | if ( $primary_term instanceof WP_Term ) { 101 | $term_link = get_term_link( $primary_term ); 102 | $term = [ 103 | 'term_name' => $primary_term->name, 104 | 'term_id' => $primary_term->term_id, 105 | 'term_link' => ! is_wp_error( $term_link ) ? $term_link : '', 106 | ]; 107 | } 108 | } 109 | /** 110 | * Filters the primary term for the post. 111 | * 112 | * @param array $term The primary term for the post. 113 | * @param int $id The post ID. 114 | * @param string $taxonomy The taxonomy. 115 | * 116 | * @phpstan-param array{term_name?: string, term_id?: int, term_link?: string} $term 117 | */ 118 | return apply_filters( 'create_wordpress_plugin_primary_term', $term, $post_id, $taxonomy ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Create WordPress Project 2 | 3 | Thank you for your interest in contributing to Create WordPress Project. 4 | 5 | Read our [Code of Conduct](https://github.com/alleyinteractive/.github/blob/main/CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | Use the table of contents icon on the top left corner of this document to get to a specific section of this guide quickly. 10 | 11 | ## New Contributor Guide 12 | 13 | To get an overview of the project, read the [README](readme.md). 14 | 15 | Here are some resources to help you get started with open source contributions: 16 | 17 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 18 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 19 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 20 | 21 | ## Getting Started 22 | 23 | ### Issues 24 | 25 | #### Create a New Issue 26 | 27 | If you see a problem or an opportunity to create a new feature, [search if an issue already exists](https://github.com/alleyinteractive/create-wordpress-project/issues). 28 | 29 | If a related issue doesn't exist, you can [open a new issue](https://github.com/alleyinteractive/create-wordpress-project/issues/new). 30 | 31 | #### Solve an Issue 32 | 33 | If you would like to help solve an existing issue navigate to the [list of open issues](https://github.com/alleyinteractive/create-wordpress-project/issues) and choose one that interests you. 34 | 35 | ### Making Changes 36 | 37 | #### Branching Workflow 38 | 39 | If you are a member of Alley, you can create a new feature branch in this repo according to our branch naming conventions. If you are not a member of Alley, you should first fork this repository, then make your changes in a branch on your fork. In either case, once you have completed your changes, create a pull request against the `develop` branch of this repository. 40 | 41 | #### Commit Your Update 42 | 43 | Commit the changes once you are happy with them. Once your changes are ready, don't forget to self-review to speed up the review process. The self-review should ensure that all automated tests and linting checks pass. See the [GitHub Workflows directory](.github/workflows) for a list of all automated tests that are executed when a PR is created and ensure that they pass locally before creating your PR. 44 | 45 | #### Pull Request 46 | 47 | When you're finished with the changes, create a pull request, also known as a PR. 48 | 49 | - Add a description of what the change does. 50 | - Don't forget to [link your PR to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one. 51 | - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. Once you submit your PR, an Alley team member will review your proposal. We may ask questions or request additional information. 52 | - We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. 53 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). 54 | - If you run into any merge issues, check out this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues. 55 | 56 | #### Your PR is Merged! 57 | 58 | Congratulations! The Alley team thanks you. 59 | 60 | Once your PR is merged, your contributions will be publicly available on the `develop` branch of the project, included in a future tagged version, and you will be added to the contributors list. 61 | 62 | ## References 63 | 64 | This contributing guide was based in part off of the [GitHub Docs Contributing Guide](https://raw.githubusercontent.com/github/docs/main/CONTRIBUTING.md). 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /configure.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ] 15 | * : The project name. 16 | * 17 | * [--author_name=] 18 | * : The author name. 19 | * 20 | * [--author_email=] 21 | * : The author email. 22 | * 23 | * phpcs:disable 24 | */ 25 | 26 | namespace Create_WordPress_Project\Configure; 27 | 28 | if ( ! defined( 'STDIN' ) ) { 29 | die( 'Not in CLI mode.' ); 30 | } 31 | 32 | if ( 0 === strpos( strtoupper( PHP_OS ), 'WIN' ) ) { 33 | echo "This script may not work in Windows. 🪟\n"; 34 | } 35 | 36 | if ( version_compare( PHP_VERSION, '8.3.0', '<' ) ) { 37 | die( 'PHP 8.3.0 or greater is required.' ); 38 | } 39 | 40 | // Parse the command line arguments from $argv. 41 | $args = []; 42 | $previous_key = null; 43 | 44 | foreach ( $argv as $value ) { 45 | if ( str_starts_with( $value, '--' ) ) { 46 | if ( false !== strpos( $value, '=' ) ) { 47 | [ $arg, $value ] = explode( '=', substr( $value, 2 ), 2 ); 48 | 49 | $args[ $arg ] = trim( $value ); 50 | 51 | $previous_key = null; 52 | } else { 53 | $args[ substr( $value, 2 ) ] = true; 54 | 55 | $previous_key = substr( $value, 2 ); 56 | } 57 | } elseif ( ! empty( $previous_key ) ) { 58 | $args[ $previous_key ] = trim( $value ); 59 | } else { 60 | $previous_key = trim( $value ); 61 | } 62 | } 63 | 64 | $terminal_width = (int) exec( 'tput cols' ); 65 | 66 | if ( ! $terminal_width ) { 67 | $terminal_width = 80; 68 | } 69 | 70 | function write( string $text ): void { 71 | global $terminal_width; 72 | echo wordwrap( $text, $terminal_width - 1 ) . PHP_EOL; 73 | } 74 | 75 | function ask( string $question, string $default = '', bool $allow_empty = true ): string { 76 | while ( true ) { 77 | write( $question . ( $default ? " [{$default}]" : '' ) . ': ' ); 78 | $answer = readline( '> ' ); 79 | 80 | $value = $answer ?: $default; 81 | 82 | if ( ! $allow_empty && empty( $value ) ) { 83 | echo "This value can't be empty." . PHP_EOL; 84 | continue; 85 | } 86 | 87 | return $value; 88 | } 89 | } 90 | 91 | function confirm( string $question, bool $default = false ): bool { 92 | write( "{$question} (yes/no) [" . ( $default ? 'yes' : 'no' ) . ']: ' ); 93 | 94 | $answer = readline( '> ' ); 95 | 96 | if ( ! $answer ) { 97 | return $default; 98 | } 99 | 100 | return in_array( strtolower( trim( $answer ) ), [ 'y', 'yes', 'true', '1' ], true ); 101 | } 102 | 103 | function run( string $command, ?string $dir = null, bool $exit_on_error = false ): string { 104 | $command = $dir ? "cd {$dir} && {$command}" : $command; 105 | 106 | $result_code = null; 107 | $output = []; 108 | 109 | exec( $command, $output, $result_code ); 110 | 111 | if ( 0 !== $result_code ) { 112 | echo "Command failed: {$command}\n"; 113 | echo "Exit code: {$result_code}\n"; 114 | echo "Output:\n"; 115 | echo implode( PHP_EOL, $output ) . PHP_EOL; 116 | 117 | if ( $exit_on_error ) { 118 | exit( 1 ); 119 | } 120 | } 121 | 122 | return trim( implode( PHP_EOL, $output ) ); 123 | } 124 | 125 | function str_after( string $subject, string $search ): string { 126 | $pos = strrpos( $subject, $search ); 127 | 128 | if ( $pos === false ) { 129 | return $subject; 130 | } 131 | 132 | return substr( $subject, $pos + strlen( $search ) ); 133 | } 134 | 135 | function slugify( string $subject ): string { 136 | return trim( (string) preg_replace( '/[^a-z0-9-]+/', '-', strtolower( $subject ) ), '-' ); 137 | } 138 | 139 | function title_case( string $subject ): string { 140 | return ensure_capitalp( str_replace( ' ', '_', ucwords( str_replace( [ '-', '_' ], ' ', $subject ) ) ) ); 141 | } 142 | 143 | function ensure_capitalp( string $text ): string { 144 | return str_replace( 'Wordpress', 'WordPress', $text ); 145 | } 146 | 147 | /** 148 | * @param string $file 149 | * @param array $replacements 150 | */ 151 | function replace_in_file( string $file, array $replacements ): void { 152 | $contents = file_get_contents( $file ); 153 | 154 | if ( empty( $contents ) ) { 155 | return; 156 | } 157 | 158 | file_put_contents( 159 | $file, 160 | str_replace( 161 | array_keys( $replacements ), 162 | array_values( $replacements ), 163 | $contents, 164 | ) 165 | ); 166 | } 167 | 168 | /** 169 | * Replace a section of a file, including the start and end delimiters and trailing whitespace. 170 | * 171 | * @param string $file Filename. 172 | * @param string $start Start string included in replacement. 173 | * @param string $end End string included in replacement. 174 | * @param string $replace String to replace content with. 175 | */ 176 | function replace_section_in_file( string $file, string $start, string $end, string $replace = '' ) { 177 | $contents = file_get_contents( $file ); 178 | 179 | if ( empty( $contents ) ) { 180 | return; 181 | } 182 | 183 | $start = preg_quote( $start, '/' ); 184 | $end = preg_quote( $end, '/' ); 185 | $regex = '/' . $start . '.*?' . $end . '\s*/s'; 186 | $result = preg_replace( $regex, $replace, $contents ); 187 | 188 | if ( $result !== $contents ) { 189 | file_put_contents( $file, $result ); 190 | } 191 | } 192 | 193 | function remove_readme_paragraphs( string $file ): void { 194 | $contents = file_get_contents( $file ); 195 | 196 | if ( empty( $contents ) ) { 197 | return; 198 | } 199 | 200 | file_put_contents( 201 | $file, 202 | trim( (string) preg_replace( '/.*?/s', '', $contents ) ?: $contents ), 203 | ); 204 | } 205 | 206 | function normalize_path_separator( string $path ): string { 207 | return str_replace( '/', DIRECTORY_SEPARATOR, $path ); 208 | } 209 | 210 | /** 211 | * @return array 212 | */ 213 | function list_all_files_for_replacement(): array { 214 | $exclude = [ 215 | 'LICENSE', 216 | 'configure.php', 217 | '.phpunit.result.cache', 218 | '.phpcs', 219 | 'composer.lock', 220 | ]; 221 | 222 | $exclude_dirs = [ 223 | '.git', 224 | 'pantheon-mu-plugin', 225 | 'vendor', 226 | 'node_modules', 227 | '.phpcs', 228 | ]; 229 | 230 | $exclude = array_map( 231 | fn ( string $file ) => "--exclude {$file}", 232 | $exclude, 233 | ); 234 | 235 | $exclude_dirs = array_map( 236 | fn ( string $dir ) => "--exclude-dir {$dir}", 237 | $exclude_dirs, 238 | ); 239 | 240 | return explode( 241 | PHP_EOL, 242 | run( 243 | "grep -R -l . " . implode( ' ', $exclude_dirs ) . ' ' . implode( ' ', $exclude ), 244 | ), 245 | ); 246 | } 247 | 248 | /** 249 | * Gets the subfolders of a given path. 250 | * 251 | * @param string $path 252 | * @return array 253 | */ 254 | function list_subfolders( $path ): array { 255 | $path = escapeshellarg($path); 256 | return explode( 257 | PHP_EOL, 258 | run( 259 | "find " . $path . " -type d -maxdepth 1 -mindepth 1", 260 | ), 261 | ); 262 | } 263 | 264 | /** 265 | * @param string|array $paths 266 | */ 267 | function delete_files( string|array $paths ): void { 268 | if ( ! is_array( $paths ) ) { 269 | $paths = [ $paths ]; 270 | } 271 | 272 | foreach ( $paths as $path ) { 273 | $path = normalize_path_separator( $path ); 274 | 275 | if ( is_dir( $path ) ) { 276 | run( "rm -rf {$path}" ); 277 | } elseif ( file_exists( $path ) ) { 278 | @unlink( $path ); 279 | } 280 | } 281 | } 282 | 283 | /** 284 | * Install a plugin with composer 285 | * 286 | * @param array $plugin_data The plugin information. 287 | * @param bool $prompt Whether or not to prompt to install. 288 | * @param array $installed_plugins The list of installed plugins. 289 | */ 290 | function install_plugin( array $plugin_data, bool $prompt, &$installed_plugins ): void { 291 | $plugin_name = $plugin_data['name']; 292 | $plugin_path = $plugin_data['path']; 293 | $plugin_repo = isset( $plugin_data['repo'] ) ? $plugin_data['repo'] : null; 294 | $repo_type = isset( $plugin_data['repo_type'] ) ? $plugin_data['repo_type'] : 'github'; 295 | $auto_activate = isset( $plugin_data['activate'] ) ? $plugin_data['activate'] : true; 296 | 297 | if ( $prompt && ! confirm( "Install {$plugin_name}?", true ) ) { 298 | return; 299 | } 300 | 301 | write( "Installing {$plugin_name}..." ); 302 | $plugin_short_name = str_after( $plugin_path, '/' ); 303 | 304 | if ( ! empty( $plugin_repo ) ) { 305 | run( "composer config repositories.{$plugin_short_name} {$repo_type} {$plugin_repo}" ); 306 | } 307 | 308 | run( "composer require -W --no-interaction --quiet {$plugin_path} --ignore-platform-req=ext-redis" ); 309 | if ( $auto_activate ) { 310 | array_push( $installed_plugins, $plugin_short_name ); 311 | } 312 | } 313 | 314 | /** 315 | * Extract package.json dependencies and dev dependencies before truncating. 316 | * 317 | * @param string $file The absolute path to the package.json file to be modified. 318 | * @return array Extracted dependencies. 319 | */ 320 | function extract_dependencies_from_package_json( string $file ): array { 321 | $json = json_decode( file_get_contents( $file ), true ); 322 | 323 | $extracted = [ 324 | 'dependencies' => $json['dependencies'] ?? [], 325 | 'devDependencies' => $json['devDependencies'] ?? [], 326 | 'engines' => $json['engines'] ?? null, 327 | ]; 328 | 329 | return $extracted; 330 | } 331 | 332 | /** 333 | * Merge extracted dependencies into the root package.json 334 | * 335 | * @param array $all_dependencies Array of extracted dependencies to merge. 336 | */ 337 | function merge_dependencies_to_root_package_json( array $all_dependencies ): void { 338 | $root_package_path = getcwd() . '/package.json'; 339 | $root_package = json_decode( file_get_contents( $root_package_path ), true ); 340 | 341 | // Merge dependencies 342 | $dependencies = []; 343 | $devDependencies = []; 344 | $engines = null; 345 | 346 | foreach ( $all_dependencies as $extracted ) { 347 | // Merge regular dependencies 348 | foreach ( $extracted['dependencies'] as $name => $version ) { 349 | $dependencies[$name] = $version; 350 | } 351 | 352 | // Merge dev dependencies 353 | foreach ( $extracted['devDependencies'] as $name => $version ) { 354 | $devDependencies[$name] = $version; 355 | } 356 | 357 | // Use the latest engines specification if available 358 | if ( $extracted['engines'] ) { 359 | $engines = $extracted['engines']; 360 | } 361 | } 362 | 363 | // Remove duplicates between dependencies and devDependencies 364 | foreach ( $dependencies as $name => $version ) { 365 | if ( isset( $devDependencies[$name] ) ) { 366 | // Keep the higher version 367 | if ( version_compare( preg_replace('/[^0-9.]/', '', $version), preg_replace('/[^0-9.]/', '', $devDependencies[$name]), '>=' ) ) { 368 | unset( $devDependencies[$name] ); 369 | } else { 370 | unset( $dependencies[$name] ); 371 | } 372 | } 373 | } 374 | 375 | // Update the root package.json 376 | $root_package['dependencies'] = array_merge( $root_package['dependencies'] ?? [], $dependencies ); 377 | $root_package['devDependencies'] = array_merge( $root_package['devDependencies'] ?? [], $devDependencies ); 378 | 379 | if ( $engines ) { 380 | $root_package['engines'] = $engines; 381 | } 382 | 383 | // Sort dependencies alphabetically 384 | ksort( $root_package['dependencies'] ); 385 | ksort( $root_package['devDependencies'] ); 386 | 387 | file_put_contents( $root_package_path, json_encode( $root_package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); 388 | } 389 | 390 | /** 391 | * A helper function to remove certain keys from package.json files in the plugin and theme. 392 | * 393 | * @param string $file The absolute path to the package.json file to be modified. 394 | */ 395 | function truncate_package_json( string $file ): void { 396 | $json = json_decode( file_get_contents( $file ), true ); 397 | 398 | unset( $json['dependencies'] ); 399 | unset( $json['devDependencies'] ); 400 | unset( $json['engines'] ); 401 | unset( $json['scripts']['check-types'] ); 402 | unset( $json['scripts']['packages-update'] ); 403 | unset( $json['scripts']['postinstall'] ); 404 | unset( $json['scripts']['release'] ); 405 | unset( $json['scripts']['test'] ); 406 | 407 | file_put_contents( $file, json_encode( $json, JSON_PRETTY_PRINT ) ); 408 | } 409 | 410 | echo "\nWelcome friend to alleyinteractive/create-wordpress-project! 😀\nLet's setup your WordPress Project 🚀\n\n"; 411 | 412 | $current_dir = getcwd(); 413 | 414 | if ( ! $current_dir ) { 415 | echo "Could not determine current directory.\n"; 416 | exit( 1 ); 417 | } 418 | 419 | // Determine the folder name from the parent directory 420 | // (this project is assumed to be at the wp-content level). 421 | $folder_name = ensure_capitalp( basename( dirname( $current_dir, 1) ) ); 422 | 423 | while ( true ) { 424 | $project_name = ask( 425 | question: 'Project name?', 426 | default: (string) ( $args['project_name'] ?? str_replace( '_', ' ', title_case( $folder_name ) ) ), 427 | allow_empty: false, 428 | ); 429 | 430 | if ( false !== strpos( $project_name, '-' ) ) { 431 | write( 'This should be a project name and not a slug. For example, "Example Project" would be a great project name. After this step, we\'ll prompt you for the project slug.' ); 432 | 433 | if ( ! confirm( 'Do you wish to continue anyway?', false ) ) { 434 | continue; 435 | } 436 | } 437 | 438 | break; 439 | } 440 | 441 | $project_name_slug = slugify( ask( 442 | question: 'Project slug?', 443 | default: slugify( $project_name ), 444 | allow_empty: false, 445 | ) ); 446 | 447 | $description = ask( 'Project description?', "{$project_name} Website" ); 448 | 449 | $username_guess = explode( ':', run( 'git config remote.origin.url' ) )[1] ?? ''; 450 | $username_guess = dirname( $username_guess ); 451 | $username_guess = basename( $username_guess ); 452 | 453 | $vendor_name = ask( 454 | question: 'Vendor/organization name (usually the Github Organization)?', 455 | default: $username_guess, 456 | allow_empty: false, 457 | ); 458 | 459 | $vendor_slug = slugify( $vendor_name ); 460 | 461 | $author_email = ask( 462 | question: 'Author email?', 463 | default: (string) ( $args['author_email'] ?? run( 'git config user.email' ) ), 464 | allow_empty: false, 465 | ); 466 | 467 | $author_username = ask( 468 | question: 'Author username?', 469 | default: $username_guess, 470 | allow_empty: false, 471 | ); 472 | 473 | $author_name = ask( 474 | question: 'Author name?', 475 | default: (string) ( $args['author_name'] ?? run( 'git config user.name' ) ), 476 | allow_empty: false, 477 | ); 478 | 479 | $plugin_slug = slugify( 480 | ask( 481 | question: 'Project plugin name?', 482 | default: $project_name_slug, 483 | allow_empty: false, 484 | ), 485 | ); 486 | 487 | if ( is_dir( "plugins/{$plugin_slug}" ) ) { 488 | write( "Plugin already exists in plugins/{$plugin_slug}." ); 489 | exit( 1 ); 490 | } 491 | 492 | $mantle = confirm( 'Should this be a Mantle plugin?', false ); 493 | 494 | $plugin_namespace = title_case( $plugin_slug ) . '_Plugin'; 495 | $year = date( 'Y' ); 496 | 497 | $theme_slug = slugify( 498 | ask( 499 | question: 'Project theme name?', 500 | default: "$project_name_slug-$year", 501 | allow_empty: false, 502 | ), 503 | ); 504 | 505 | if ( is_dir( "themes/{$theme_slug}" ) ) { 506 | write( "Theme already exists in themes/{$theme_slug}." ); 507 | exit( 1 ); 508 | } 509 | 510 | $theme_namespace = title_case( $theme_slug ) . '_Theme'; 511 | 512 | $slack_channel_id = ask( 513 | question: 'Slack Channel ID? (for deploy notifications)', 514 | allow_empty: true, 515 | ); 516 | 517 | $slack_channel_name = ask( 518 | question: 'Slack Channel Name? (for deploy notifications)', 519 | allow_empty: true, 520 | ); 521 | 522 | write( '------' ); 523 | write( "Project : {$project_name} <{$project_name_slug}>" ); 524 | write( "Description : {$description}" ); 525 | write( "Author : {$author_name} ({$author_email})" ); 526 | write( "Vendor : {$vendor_name} ({$vendor_slug})" ); 527 | 528 | if ( ! empty( $plugin_slug ) ) { 529 | write( "Plugin : plugins/{$plugin_slug}" ); 530 | write( "Plugin Namespace : {$plugin_namespace}" ); 531 | } 532 | 533 | 534 | if ( ! empty( $theme_slug ) ) { 535 | write( "Theme : themes/{$theme_slug}" ); 536 | write( "Theme Namespace : {$theme_namespace}" ); 537 | } 538 | 539 | if ( ! empty( $slack_channel_id ) ) { 540 | write( "Slack Channel ID : {$slack_channel_id}" ); 541 | } 542 | 543 | if ( ! empty( $slack_channel_name ) ) { 544 | write( "Slack Channel Name : {$slack_channel_name}" ); 545 | } 546 | 547 | write( '------' ); 548 | 549 | write( 'This script will replace the above values in all relevant files in the project directory.' ); 550 | 551 | if ( ! confirm( 'Modify files?', true ) ) { 552 | exit( 1 ); 553 | } 554 | 555 | $search_and_replace = [ 556 | 'author_name' => $author_name, 557 | 'author_username' => $author_username, 558 | 'email@domain.com' => $author_email, 559 | 560 | 'A skeleton WordPress project' => $description, 561 | 562 | 'create-wordpress-project' => $project_name_slug, 563 | 'Create WordPress Project' => $project_name, 564 | 'CREATE_WORDPRESS_PROJECT' => strtoupper( str_replace( '-', '_', $project_name_slug ) ), 565 | 566 | 'vendor_name' => $vendor_name, 567 | 'vendor_slug' => $vendor_slug, 568 | ]; 569 | 570 | /* 571 | * Hardcoded strings we need to replace. 572 | * These are very specific and should be used sparingly. 573 | */ 574 | $hardcoded_strings = [ 575 | // Replace the composer project name. 576 | 'alleyinteractive/create-wordpress-project' => $vendor_slug . '/' . $project_name_slug, 577 | ]; 578 | 579 | $search_and_replace = array_merge( 580 | $search_and_replace, 581 | $hardcoded_strings, 582 | ); 583 | 584 | if ( ! empty( $theme_slug ) ) { 585 | $search_and_replace = array_merge( 586 | $search_and_replace, 587 | [ 588 | 'create-wordpress-theme' => $theme_slug, 589 | 'Create WordPress Theme' => str_replace( '_', ' ', title_case( $theme_slug ) ), 590 | 'CREATE_WORDPRESS_THEME' => strtoupper( str_replace( '-', '_', $theme_slug ) ), 591 | 'create_wordpress_theme' => str_replace( '-', '_', $theme_slug ), 592 | 'Create_WordPress_Theme' => $theme_namespace, 593 | ], 594 | ); 595 | } 596 | 597 | if ( ! empty( $plugin_slug ) ) { 598 | $search_and_replace = array_merge( 599 | $search_and_replace, 600 | [ 601 | 'create-wordpress-plugin' => $plugin_slug, 602 | 'Create WordPress Plugin' => str_replace( '_', ' ', title_case( $plugin_slug ) ), 603 | 'CREATE_WORDPRESS_PLUGIN' => strtoupper( str_replace( '-', '_', $plugin_slug ) ), 604 | 'create_wordpress_plugin' => str_replace( '-', '_', $plugin_slug ), 605 | 'Create_WordPress_Plugin' => $plugin_namespace, 606 | ] 607 | ); 608 | } 609 | 610 | if ( ! empty( $slack_channel_id ) ) { 611 | $search_and_replace = array_merge( 612 | $search_and_replace, 613 | [ 614 | 'slack_channel_id' => $slack_channel_id, 615 | ] 616 | ); 617 | } 618 | 619 | if ( ! empty( $slack_channel_name ) ) { 620 | $search_and_replace = array_merge( 621 | $search_and_replace, 622 | [ 623 | 'slack_channel_name' => $slack_channel_name, 624 | ] 625 | ); 626 | } 627 | 628 | run( 629 | 'composer config extra.wordpress-autoloader.autoload --json \'' . json_encode( [ 630 | $plugin_namespace => "plugins/{$plugin_slug}/src", 631 | $theme_namespace => "themes/{$theme_slug}/src", 632 | ] ) . '\'', 633 | ); 634 | 635 | if ( ! empty( $plugin_slug ) ) { 636 | if ( $mantle ) { 637 | // Download the latest Mantle PHAR from alleyinteractive/mantle-installer releases and use it. 638 | $latest_release = json_decode( 639 | run( 'curl -s https://api.github.com/repos/alleyinteractive/mantle-installer/releases/latest' ), 640 | true, 641 | ); 642 | 643 | if ( empty( $latest_release['assets'][0]['browser_download_url'] ) ) { 644 | echo '🚨 No mantle-installer.phar found on latest release. Exiting...'; 645 | exit( 1 ); 646 | } 647 | 648 | write( "Scaffolding mantle to plugins/{$plugin_slug} via alleyinteractive/mantle-installer..." ); 649 | 650 | run( "curl -sL {$latest_release['assets'][0]['browser_download_url']} -o mantle-installer.phar && chmod +x mantle-installer.phar" ); 651 | run( "php mantle-installer.phar new {$plugin_slug} && rm mantle-installer.phar" ); 652 | } else { 653 | write( "Scaffolding create-wordpress-plugin to plugins/{$plugin_slug}..." ); 654 | 655 | run( 656 | "composer create-project alleyinteractive/create-wordpress-plugin plugins/{$plugin_slug} --no-install --prefer-source --remove-vcs", 657 | $current_dir, 658 | true, 659 | ); 660 | 661 | run( "mv plugins/{$plugin_slug}/plugin.php plugins/{$plugin_slug}/{$plugin_slug}.php" ); 662 | } 663 | 664 | // Create a .eslintignore file and ignore the "build/" directory. 665 | file_put_contents( 666 | "{$current_dir}/plugins/{$plugin_slug}/.eslintignore", 667 | "build/\n" 668 | ); 669 | 670 | // Move the contents of each subfolder in plugin-templates to the plugin folder. 671 | run( "rsync -a plugin-templates/ plugins/{$plugin_slug}/" ); 672 | 673 | // Copy the initial features from features.txt into the plugin main file. 674 | if ( file_exists( "{$current_dir}/plugins/{$plugin_slug}/src/main.php" ) ) { 675 | replace_in_file( "plugins/{$plugin_slug}/src/main.php", [ 676 | ' // Add features here.' => file_get_contents( 'plugin-templates/features.txt' ), 677 | ] ); 678 | } 679 | 680 | // Create a .eslintignore file and ignore the "build/" directory. 681 | file_put_contents( 682 | "{$current_dir}/plugins/{$plugin_slug}/.eslintignore", 683 | "build/\n" 684 | ); 685 | 686 | // Create a .stylelintignore file and ignore the "build/" directory. 687 | file_put_contents( 688 | "{$current_dir}/plugins/{$plugin_slug}/.stylelintignore", 689 | "build/\n" 690 | ); 691 | 692 | // Extract dependencies from the plugin's package.json before truncating 693 | $all_dependencies = $all_dependencies ?? []; 694 | if ( file_exists( "{$current_dir}/plugins/{$plugin_slug}/package.json" ) ) { 695 | write( "Extracting dependencies from plugin's package.json..." ); 696 | $all_dependencies[] = extract_dependencies_from_package_json( "{$current_dir}/plugins/{$plugin_slug}/package.json" ); 697 | } 698 | 699 | // Make changes to the package.json that ships with the plugin. 700 | truncate_package_json( "{$current_dir}/plugins/{$plugin_slug}/package.json" ); 701 | 702 | echo "Done!\n\n"; 703 | } 704 | 705 | if ( ! empty( $theme_slug ) ) { 706 | write( "Scaffolding create-wordpress-theme to themes/{$theme_slug}..." ); 707 | 708 | run( 709 | "composer create-project alleyinteractive/create-wordpress-theme themes/{$theme_slug} --no-install --prefer-source --remove-vcs", 710 | $current_dir, 711 | true, 712 | ); 713 | 714 | // Create a .eslintignore file and ignore the "build/" directory. 715 | file_put_contents( 716 | "{$current_dir}/themes/{$theme_slug}/.eslintignore", 717 | "build/\n" 718 | ); 719 | 720 | // Create a .stylelintignore file and ignore the "build/" directory. 721 | file_put_contents( 722 | "{$current_dir}/themes/{$theme_slug}/.stylelintignore", 723 | "build/\n" 724 | ); 725 | 726 | // Extract dependencies from the theme's package.json before truncating 727 | if ( file_exists( "{$current_dir}/themes/{$theme_slug}/package.json" ) ) { 728 | write( "Extracting dependencies from theme's package.json..." ); 729 | $all_dependencies[] = extract_dependencies_from_package_json( "{$current_dir}/themes/{$theme_slug}/package.json" ); 730 | } 731 | 732 | // Make changes to the package.json that ships with the theme. 733 | truncate_package_json( "{$current_dir}/themes/{$theme_slug}/package.json" ); 734 | 735 | run( 736 | "wp theme activate {$theme_slug}", 737 | $current_dir, 738 | ); 739 | } 740 | 741 | // Merge all extracted dependencies into the root package.json 742 | if ( !empty( $all_dependencies ) ) { 743 | write( "Merging extracted dependencies to root package.json..." ); 744 | merge_dependencies_to_root_package_json( $all_dependencies ); 745 | } 746 | 747 | foreach ( list_all_files_for_replacement() as $path ) { 748 | echo "Updating $path...\n"; 749 | 750 | replace_in_file( $path, $search_and_replace ); 751 | 752 | if ( str_contains( $path, 'README.md' ) ) { 753 | remove_readme_paragraphs( $path ); 754 | } 755 | } 756 | 757 | echo "Done!\n\n"; 758 | 759 | write( 'Running composer update...' ); 760 | 761 | run( 'composer update' ); 762 | 763 | // Move the ci-templates to the root of the project. 764 | if ( is_dir( 'ci-templates' ) ) { 765 | write( "Moving ci-templates files to the root of the project and removing ci-templates directory..." ); 766 | 767 | run( 'rm -rf .github && rsync -a ci-templates/ ./ && rm -rf ci-templates' ); 768 | } 769 | 770 | write( 'Removing configuration script from theme/plugin...' ); 771 | 772 | delete_files( 773 | [ 774 | "themes/{$theme_slug}/configure.php", 775 | "themes/{$theme_slug}/Makefile", 776 | "plugins/{$plugin_slug}/configure.php", 777 | "plugins/{$plugin_slug}/Makefile", 778 | 'plugin-templates', 779 | '.github/workflows/action.yml', 780 | ] 781 | ); 782 | 783 | echo "Done!\n\n"; 784 | 785 | $hosting_provider = null; 786 | 787 | // Determine the hosting provider we'll be using. 788 | if ( confirm( 'Will this project be hosted on WordPress VIP?' ) ) { 789 | $hosting_provider = 'vip'; 790 | } elseif ( confirm( 'Will this project be hosted on Pantheon?', true ) ) { 791 | $hosting_provider = 'pantheon'; 792 | } 793 | 794 | // Prompt the user to convert the folder structure to WordPress VIP. 795 | if ( 'vip' === $hosting_provider ) { 796 | $vip_repo_name = ask( 797 | question: 'VIP Repository Name?', 798 | default: $project_name_slug, 799 | allow_empty: false, 800 | ); 801 | 802 | replace_in_file( 803 | '.github/workflows/copy-to-vip.yml', 804 | [ 805 | 'VIP_REPO_SLUG' => $vip_repo_name, 806 | ], 807 | ); 808 | 809 | replace_in_file( 810 | '.github/workflows/deploy-to-vip-built-branch.yml', 811 | [ 812 | 'VIP_REPO_SLUG' => $vip_repo_name, 813 | ], 814 | ); 815 | 816 | write( 'Deleting Pantheon-specific GitHub Action workflows...' ); 817 | 818 | delete_files( 819 | [ 820 | '.github/workflows/deploy-to-pantheon.yml', 821 | "plugins/$plugin_slug/$vendor_slug", 822 | ] 823 | ); 824 | 825 | write( 'Moving mu-plugins to client-mu-plugins...' ); 826 | run( 'mv mu-plugins client-mu-plugins' ); 827 | run( 'composer config vendor-dir client-mu-plugins/vendor' ); 828 | 829 | write( 'Ignoring mu-plugins with .gitignore/.deployignore...' ); 830 | 831 | file_put_contents( '.gitignore', 'vip' === $hosting_provider ? 'client-mu-plugins\n' : 'mu-plugins\n', FILE_APPEND ); 832 | file_put_contents( '.deployignore', 'vip' === $hosting_provider ? 'client-mu-plugins\n' : 'mu-plugins\n', FILE_APPEND ); 833 | 834 | write( 'Removing pantheon-systems/pantheon-mu-plugin from project\'s composer.json...' ); 835 | 836 | run( 'composer remove pantheon-systems/pantheon-mu-plugin' ); 837 | delete_files( 'client-mu-plugins/pantheon-mu-plugin' ); 838 | 839 | replace_in_file( 840 | 'phpstan.neon', 841 | [ 842 | 'mu-plugins/' => 'client-mu-plugins/', 843 | ], 844 | ); 845 | 846 | // Remove the pantheon mu-plugin from the plugin loader file. 847 | replace_in_file( 848 | 'client-mu-plugins/plugin-loader.php', 849 | [ 850 | "\n// Load Pantheon's mu-plugin.\nrequire_once __DIR__ . '/pantheon-mu-plugin/pantheon.php';\n" => '', 851 | ], 852 | ); 853 | 854 | write( 'Running composer update...' ); 855 | 856 | run( 'composer update' ); 857 | 858 | write( 'Cloning Automattic/vip-go-mu-plugins-built to mu-plugins...' ); 859 | 860 | run( 'git clone git@github.com:Automattic/vip-go-mu-plugins-built.git mu-plugins' ); 861 | 862 | if ( ! is_file( __DIR__ . '/object-cache.php' ) ) { 863 | write( 'Symlinking object-cache.php from mu-plugins to wp-content/object-cache.php...' ); 864 | 865 | run( 'ln -s mu-plugins/drop-ins/object-cache.php object-cache.php' ); 866 | } 867 | 868 | write( 'Scaffolding out vip-config...' ); 869 | 870 | run( 'mkdir -p vip-config && touch vip-config/.gitkeep' ); 871 | 872 | write( 'Scaffolding out VIP directories...' ); 873 | 874 | run( 'mkdir -p images && touch images/.gitkeep' ); 875 | run( 'mkdir -p languages && touch languages/.gitkeep' ); 876 | run( 'mkdir -p private && touch private/.gitkeep' ); 877 | 878 | echo "Done!\n\n"; 879 | } elseif ( 'pantheon' === $hosting_provider ) { 880 | write( 'Deleting VIP-specific GitHub Action workflows...' ); 881 | 882 | delete_files( 883 | [ 884 | '.github/workflows/copy-to-vip.yml', 885 | '.github/workflows/deploy-to-vip-built-branch.yml', 886 | ] 887 | ); 888 | 889 | echo "Done!\n\n"; 890 | } 891 | 892 | // Track the installed plugins, so we can activate them. 893 | $installed_plugins = []; 894 | 895 | write( 'Installing Required Plugins...' ); 896 | $required_file_contents = file_get_contents( 'composer-templates/default.json' ); 897 | $required_plugins = json_decode( $required_file_contents, true ); 898 | foreach( $required_plugins as $plugin ) { 899 | install_plugin( $plugin, false, $installed_plugins ); 900 | } 901 | 902 | write( 'Installing Suggested Plugins...' ); 903 | $suggested_file_contents = file_get_contents( 'composer-templates/suggested.json' ); 904 | $suggested_plugins = json_decode( $suggested_file_contents, true ); 905 | if ( $hosting_provider === 'vip' ) { 906 | // Plugins already installed on VIP. 907 | $suggested_plugins = array_filter( 908 | $suggested_plugins, 909 | fn ( $plugin ) => $plugin['path'] !== 'alleyinteractive/es-wp-query', 910 | ); 911 | } 912 | foreach( $suggested_plugins as $plugin ) { 913 | install_plugin( $plugin, true, $installed_plugins ); 914 | } 915 | 916 | if ( 'pantheon' === $hosting_provider ) { 917 | write( 'Installing Pantheon Plugins...' ); 918 | $license_key = ask( 919 | question: 'Object Cache Pro License Key? Run \'terminus remote:wp "." -- eval "echo getenv(\'OCP_LICENSE\');"\' to get the license key. (Leave blank to skip)', 920 | allow_empty: true, 921 | ); 922 | 923 | $pantheon_file_contents = file_get_contents( 'composer-templates/pantheon.json' ); 924 | $pantheon_plugins = json_decode( $pantheon_file_contents, true ); 925 | foreach( $pantheon_plugins as $plugin ) { 926 | install_plugin( $plugin, false, $installed_plugins ); 927 | } 928 | if ( ! empty( $license_key ) ) { 929 | run( "mv composer-templates/auth.json ./auth.json" ); 930 | replace_in_file( 'auth.json', [ 'object_cache_pro_token' => $license_key ] ); 931 | 932 | install_plugin( 933 | [ 934 | 'name' => 'Object Cache Pro', 935 | 'repo' => 'https://objectcache.pro/repo/', 936 | 'repo_type' => 'composer', 937 | 'path' => 'rhubarbgroup/object-cache-pro' 938 | ], 939 | false, 940 | $installed_plugins 941 | ); 942 | } 943 | } 944 | 945 | // Automatically activate the installed plugins. 946 | $plugin_files = array_filter( 947 | array_map( function( $plugin_dir ) { 948 | $file_names = [ 949 | $plugin_dir.'php', 950 | strtolower($plugin_dir).'.php', 951 | 'plugin.php', 952 | 'index.php', 953 | ]; 954 | // Include some one-off exceptions. 955 | if ( strpos( $plugin_dir, 'wp-' ) === 0) { 956 | $file_names[] = substr( $plugin_dir, 3 ).'.php'; 957 | $file_names[] = 'wordpress-'.substr( $plugin_dir, 3 ).'.php'; 958 | } elseif ( strpos( $plugin_dir, 'wordpress-' ) === 0) { 959 | $file_names[] = substr( $plugin_dir, 10 ).'.php'; 960 | $file_names[] = 'wp-'.substr( $plugin_dir, 10 ).'.php'; 961 | } 962 | 963 | foreach ( $file_names as $file ) { 964 | // Check if the file exists and is not (virtually) empty. 965 | if ( file_exists( "plugins/{$plugin_dir}/{$file}" ) && 50 < filesize( "plugins/{$plugin_dir}/{$file}" ) ) { 966 | return "'{$plugin_dir}/{$file}',"; 967 | } 968 | } 969 | return null; 970 | }, 971 | $installed_plugins ) 972 | ); 973 | sort( $plugin_files ); 974 | $plugin_files[] = "'{$plugin_slug}/{$plugin_slug}.php',"; 975 | 976 | replace_in_file( 977 | 'vip' === $hosting_provider ? 'client-mu-plugins/plugin-loader.php' : 'mu-plugins/plugin-loader.php', 978 | [ 979 | "'{$plugin_slug}/{$plugin_slug}.php'," => implode( "\n\t\t", $plugin_files ), 980 | ] 981 | ); 982 | 983 | // Delete the composer-templates directory. 984 | delete_files( [ 'composer-templates' ] ); 985 | 986 | // Clean up .gitignore. 987 | replace_section_in_file( '.gitignore', '# BEGIN DELETE AFTER INSTALL #', '# END DELETE AFTER INSTALL #' ); 988 | 989 | if ( confirm( 'Let this script delete itself?', true ) ) { 990 | delete_files( [ 'Makefile', __FILE__ ] ); 991 | } 992 | 993 | echo "\n\nWe're done! 🎉\n\n"; 994 | 995 | die( 0 ); 996 | --------------------------------------------------------------------------------