Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create block: Add support for external templates hosted on npm #22175

Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions packages/create-block/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ $ npm init @wordpress/block [options] [slug]
Options:
```
-V, --version output the version number
-t, --template <name> template type name, allowed values: "es5", "esnext" (default: "esnext")
-t, --template <name> template name: "es5", "esnext" or the name of an external npm package (default: "esnext")
--namespace <value> internal namespace for the block name
--title <value> display title for the block
--short-description <value> short description for the block
Expand All @@ -65,9 +65,9 @@ More examples:

When you scaffold a block, you must provide at least a `slug` name, the `namespace` which usually corresponds to either the `theme` or `plugin` name, and the `category`. In most cases, we recommended pairing blocks with plugins rather than themes, because only using plugin ensures that all blocks still work when your theme changes.

## Available Commands
## Available Commands [ESNext template]

Inside that bootstrapped directory _(it doesn't apply to `es5` template)_, you can run several commands:
When bootstraped with the `esnext` templateyou can run several commands inside the directory:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing, wpScriptsEnabled enables those commands. Once we sort out the final shape of template definition we can update this section to reflect it.

Copy link
Member Author

@fabiankaegy fabiankaegy May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gziolo Yes the commands get enabled by wpScriptsEnabled but at the end it matters what the creator of the template has in their scripts in the package.json.

Thats why I changed it to only refer to the esnext template.

fabiankaegy marked this conversation as resolved.
Show resolved Hide resolved

```bash
$ npm start
Expand Down Expand Up @@ -99,6 +99,53 @@ $ npm run packages-update
```
Updates WordPress packages to the latest version. [Learn more](/packages/scripts#packages-update).

## External Templates
Since version 0.12.0 it is possible to use external templates hosted on NPM. These templates need to contain `.mustache` files that will be used in the scaffolding and one `template.json` for the metadata.

### Availabe Variables:
fabiankaegy marked this conversation as resolved.
Show resolved Hide resolved
- `namespace`
- `slug`
- `title`
- `textdomain`
- `description`
- `category`
- `dashicon`
- `license`
- `licenseURI`
- `namespaceSnakeCase`
- `slugSnakeCase`
Comment on lines +115 to +116
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namespaceSnakeCase and slugSnakeCase ar sort of special here. We should omit them and find a way to apply those modifications inside the template.


### `template.json`
```json
{
"defaultValues": {
"namespace": "create-block",
"slug": "esnext-example",
"title": "ESNext Example",
"description":
"Example block written with ESNext standard and JSX support – build step required.",
"dashicon": "smiley",
"category": "widgets",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"licenseURI": "https://www.gnu.org/licenses/gpl-2.0.html",
"version": "0.1.0"
},
"outputFiles": [
Copy link
Member

@gziolo gziolo May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the future PR:

I'm very interested to find a way to automate the discovery of templates with glob call or something like this. It would be great to be able to default to templates folder that the template author can override:

{
    "templatesFolder": "./my-folder"
}

".editorconfig",
".gitignore",
"editor.css",
"src/edit.js",
"src/index.js",
"src/save.js",
"$slug.php",
"style.css",
"readme.txt"
],
"wpScriptsEnabled": true
Copy link
Member

@gziolo gziolo May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the future PR:

Thinking about it now when documented, it should be opt-out rather than opt-in, we want people to use wp-scripts :)

}
```

## WP-CLI

Another way of making a developer’s life easier is to use [WP-CLI](https://wp-cli.org), which provides a command-line interface for many actions you might perform on the WordPress instance. One of the commands `wp scaffold block` was used as the baseline for this tool and ES5 template in particular.
Expand Down
8 changes: 5 additions & 3 deletions packages/create-block/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ program
) => {
await checkSystemRequirements( engines );
try {
const defaultValues = getDefaultValues( template );
const defaultValues = await getDefaultValues( template );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that all methods need to be converted to async here is a bit concerning. I'm coming to the conclusion that we should call async getTemplate( templateName ); first and then pass the object to all other methods. This way we avoid converting all methods to async, plus there is no need to check whether we don't download the template multiple time, or process it. I'm happy to apply all refactoring in the related PR I opened earlier.

const optionsValues = pickBy( {
category,
description: shortDescription,
Expand All @@ -62,11 +62,13 @@ program
};
await scaffold( template, answers );
} else {
const propmpts = getPrompts( template ).filter(
const propmpts = await getPrompts( template );
fabiankaegy marked this conversation as resolved.
Show resolved Hide resolved
const filteredPrompts = propmpts.filter(
( { name } ) =>
! Object.keys( optionsValues ).includes( name )
);
const answers = await inquirer.prompt( propmpts );
const answers = await inquirer.prompt( filteredPrompts );

await scaffold( template, {
...defaultValues,
...optionsValues,
Expand Down
49 changes: 28 additions & 21 deletions packages/create-block/lib/scaffold.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ const makeDir = require( 'make-dir' );
const { readFile, writeFile } = require( 'fs' ).promises;
const { render } = require( 'mustache' );
const { snakeCase } = require( 'lodash' );
const raimraf = require( 'rimraf' ).sync;
fabiankaegy marked this conversation as resolved.
Show resolved Hide resolved

/**
* Internal dependencies
*/
const initWPScripts = require( './init-wp-scripts' );
const { code, info, success } = require( './log' );
const { hasWPScriptsEnabled, getOutputFiles } = require( './templates' );
const {
hasWPScriptsEnabled,
getOutputFiles,
isCoreTemplate,
tempFolder,
} = require( './templates' );

module.exports = async function(
templateName,
Expand Down Expand Up @@ -50,34 +56,35 @@ module.exports = async function(
licenseURI,
textdomain: namespace,
};
await Promise.all(
getOutputFiles( templateName ).map( async ( file ) => {
const template = await readFile(
join(
__dirname,
`templates/${ templateName }/${ file }.mustache`
),
'utf8'
);
// Output files can have names that depend on the slug provided.
const outputFilePath = `${ slug }/${ file.replace(
/\$slug/g,
slug
) }`;
await makeDir( dirname( outputFilePath ) );
writeFile( outputFilePath, render( template, view ) );
} )
);

if ( hasWPScriptsEnabled( templateName ) ) {
const templateDirectory = isCoreTemplate( templateName )
? join( __dirname, 'templates' )
: join( tempFolder, 'node_modules' );

const outputFiles = await getOutputFiles( templateName );
outputFiles.map( async ( file ) => {
const template = await readFile(
join( templateDirectory, `${ templateName }/${ file }.mustache` ),
'utf8'
);
// Output files can have names that depend on the slug provided.
const outputFilePath = `${ slug }/${ file.replace( /\$slug/g, slug ) }`;
await makeDir( dirname( outputFilePath ) );
writeFile( outputFilePath, render( template, view ) );
} );

const wpScriptsEnabled = await hasWPScriptsEnabled( templateName );
if ( wpScriptsEnabled ) {
await initWPScripts( view );
}

raimraf( tempFolder );

info( '' );
success(
`Done: block "${ title }" bootstrapped in the "${ slug }" folder.`
);
if ( hasWPScriptsEnabled( templateName ) ) {
if ( wpScriptsEnabled ) {
info( '' );
info( 'Inside that directory, you can run several commands:' );
info( '' );
Expand Down
104 changes: 89 additions & 15 deletions packages/create-block/lib/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
*/
const CLIError = require( './cli-error' );
const prompts = require( './prompts' );
const { command } = require( 'execa' );
const { existsSync } = require( 'fs' );
const { join } = require( 'path' );
const { readFile } = require( 'fs' ).promises;
const makeDir = require( 'make-dir' );
const os = require( 'os' );
const { v4: uuid } = require( 'uuid' );

const log = require( './log' );

const namespace = 'create-block';
const dashicon = 'smiley';
Expand All @@ -12,7 +21,7 @@ const license = 'GPL-2.0-or-later';
const licenseURI = 'https://www.gnu.org/licenses/gpl-2.0.html';
const version = '0.1.0';

const templates = {
const coreTemplates = {
es5: {
defaultValues: {
namespace,
Expand Down Expand Up @@ -65,27 +74,39 @@ const templates = {
},
};

const getTemplate = ( templateName ) => {
if ( ! templates[ templateName ] ) {
const tempFolder = join( os.tmpdir(), uuid() );

const getTemplate = async ( templateName ) => {
if ( coreTemplates[ templateName ] ) {
return coreTemplates[ templateName ];
}

// throw a CLIError if the the template is neither a core one nor an external one
if ( ! ( await isExternalTemplate( templateName ) ) ) {
throw new CLIError(
`Invalid template type name. Allowed values: ${ Object.keys(
templates
).join( ', ' ) }.`
`Invalid template type name. Either use one of the Core templates: ${ Object.keys(
coreTemplates
).join( ', ' ) }. \n \n or a valid npm package name.`
fabiankaegy marked this conversation as resolved.
Show resolved Hide resolved
);
}
return templates[ templateName ];

const packageInfo = await downloadExternalTemplate( templateName );

return packageInfo;
};

const getDefaultValues = ( templateName ) => {
return getTemplate( templateName ).defaultValues;
const getDefaultValues = async ( templateName ) => {
const template = await getTemplate( templateName );
return template.defaultValues;
};

const getOutputFiles = ( templateName ) => {
return getTemplate( templateName ).outputFiles;
const getOutputFiles = async ( templateName ) => {
const template = await getTemplate( templateName );
return template.outputFiles;
};

const getPrompts = ( templateName ) => {
const defaultValues = getDefaultValues( templateName );
const getPrompts = async ( templateName ) => {
const defaultValues = await getDefaultValues( templateName );
return Object.keys( prompts ).map( ( promptName ) => {
return {
...prompts[ promptName ],
Expand All @@ -94,13 +115,66 @@ const getPrompts = ( templateName ) => {
} );
};

const hasWPScriptsEnabled = ( templateName ) => {
return getTemplate( templateName ).wpScriptsEnabled || false;
const hasWPScriptsEnabled = async ( templateName ) => {
const template = await getTemplate( templateName );
return template.wpScriptsEnabled || false;
};

const isCoreTemplate = ( templateName ) =>
coreTemplates[ templateName ] || false;

const isExternalTemplate = async ( templateName ) => {
try {
await command( `npm view ${ templateName }` );
fabiankaegy marked this conversation as resolved.
Show resolved Hide resolved
return true;
} catch ( error ) {
return false;
}
};

const downloadExternalTemplate = async ( templateName ) => {
try {
const cwd = tempFolder;

if ( existsSync( join( cwd, 'node_modules', templateName ) ) ) {
const rawPackageInfo = await readFile(
join(
tempFolder,
'node_modules',
templateName,
'template.json'
)
);
const packageInfo = JSON.parse( rawPackageInfo );

return packageInfo;
}

await makeDir( cwd );
await command( 'npm init -y', { cwd } );
await command( `npm install ${ templateName } --save`, { cwd } );

const rawPackageInfo = await readFile(
join( tempFolder, 'node_modules', templateName, 'template.json' )
);
const packageInfo = JSON.parse( rawPackageInfo );

return packageInfo;
} catch ( error ) {
log.error(
'There has been an error while trying to download the package from NPM:'
);
log.error( error );
return false;
}
};

module.exports = {
getDefaultValues,
getOutputFiles,
getPrompts,
hasWPScriptsEnabled,
isCoreTemplate,
isExternalTemplate,
tempFolder,
};
2 changes: 2 additions & 0 deletions packages/create-block/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"lodash": "^4.17.15",
"make-dir": "^3.0.0",
"mustache": "^4.0.0",
"rimraf": "^3.0.2",
"uuid": "^8.0.0",
"write-pkg": "^4.0.0"
},
"publishConfig": {
Expand Down