Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
55 changes: 37 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# Building SiteOrigin plugins
There are few steps necessary to prepare a plugin for release on the WordPress.org plugin directory. We use [Gulp](http://gulpjs.com/) to automate this.
There are a few steps necessary to prepare a plugin for release on the WordPress.org plugin directory. We use [Gulp](http://gulpjs.com/) to automate this process.

## Environment setup
1. [Download](https://nodejs.org/download/) and install Node.js and npm.
2. Install gulp using `npm install -g gulp`.
3. In the plugin folder, ensure the plugin-build repository has been added as a submodule in a folder called 'build' and is up to date. This can be done using `git submodule add git@github.com:siteorigin/plugin-build.git build`.
4. In a terminal, navigate to the build directory in the plugin and run `npm install`. When using plugin-build in the [SiteOrigin CSS](https://github.com/siteorigin/so-css) plugin, `npm-install` should be run in both the SiteOrigin CSS plugin folder and the build folder.
5. Get some coffee while npm installs the required packages.
2. Install gulp globally using `npm install -g gulp`.
3. In the plugin folder, ensure the plugin-build repository has been added as a submodule in a folder called 'build' and is up to date. This can be done using `git submodule add git@github.com:siteorigin/plugin-build.git build`.
4. In a terminal, navigate to the build directory in the plugin and run `npm install`. When using plugin-build in the [SiteOrigin CSS](https://github.com/siteorigin/so-css) plugin, `npm install` should be run in both the SiteOrigin CSS plugin folder and the build folder.
5. Grab a coffee while npm installs the required packages.

## Configuring builds
Each plugin has it's own slug and different files needed for a build. So any plugin that uses this build is required to have a configuration file called `build-config.js` in the root of the plugin directory. This file is in the form of an npm module which simply returns a configuration object used by the gulpfile to determine the files and folders needed for the build. Below is an excerpt from the SiteOrigin CSS Editor plugin `build-config.js` file:
Each plugin has its own slug and different files needed for a build. Any plugin that uses this build is required to have a configuration file called `build-config.js` in the root of the plugin directory. This file is in the form of an npm module which simply returns a configuration object used by the gulpfile to determine the files and folders needed for the build. Here's an excerpt from the SiteOrigin CSS Editor plugin `build-config.js` file:

```
```javascript
module.exports = {
slug: 'so-css',
jsMinSuffix: '.min',
Expand All @@ -29,8 +29,9 @@ module.exports = {
```

## Running builds
There are two build tasks, `build:release` and `build:dev`.
There are two main build tasks: `build:release` and `build:dev`.

### Release Build
The release task performs the following subtasks:

1. Updates the version number in the required files.
Expand All @@ -39,20 +40,38 @@ The release task performs the following subtasks:
4. Copies all files to a `dist/` folder.
5. Creates a `.zip` archive with the appropriate filename ready for uploading to wordpress.org.

Release task usage:
To run a release build, use:

```
npm run build:release --release=X.X.X
```

Replace `X.X.X` with the desired version number. For example, if the next version of the plugin is 1.2.3:

```
npm run build:release --release=1.2.3
```

### Development Build
The dev build task has one main function:

`gulp build:release -v {version}`
1. Watch LESS and/or SASS files for changes and compile them to CSS.

Where `{version}` should be replaced with the required version number.
For example, say the next version of the plugin is 1.2.3:
This eliminates the need to manually recompile LESS/SASS files while working on them.

`gulp build:release -v {1.2.3}`
To run a development build, use:

The dev build task only has one subtask:
```
npm run build:dev
```

1) Watch LESS and/or SASS files for changes and compile to CSS.
## Updating the Google Fonts Array
To update the Google Fonts array, use:

```
npm run updateGoogleFonts --apiKey=YOUR_API_KEY
```

This is simply to avoid having to manually recompile LESS/SASS files while working on them.
Replace `YOUR_API_KEY` with your actual Google Fonts API key. This task requires an update to the `build-config.js` file in each plugin to specify the name and location of the fonts file.

## Updating the Google fonts array
`gulp updateGoogleFonts ---apiKey {YOUR_API_KEY}` The task will require an update to the build-config file in each plugin to specify the name and location of the fonts file.
Remember to check the `build-config.js` file in your plugin to ensure it's correctly configured for these tasks.
34 changes: 34 additions & 0 deletions build-steps/css-tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// build-steps/css-tasks.js
import gulp from 'gulp';
const { src, dest } = gulp;
import less from 'gulp-less';
import sass from 'gulp-sass';

const catchDevErrors = (plugin) => {
if (process.env.NODE_ENV === 'development') {
plugin.on('error', (error) => {
console.error(error);
plugin.emit('end');
});
}
return plugin;
};

export const lessTask = (config, args) => {
if (!config.less) {
return Promise.resolve();
}
return src(config.less.src, { base: '.' })
.pipe(catchDevErrors(less({ paths: config.less.include, compress: args.target === 'build:release' })))
.pipe(dest(args.target === 'build:release' ? 'tmp' : '.'));
};

export const sassTask = (config, args) => {
if (!config.sass || !config.sass.src || config.sass.src.length === 0) {
console.log('No SASS files to process');
return Promise.resolve();
}
return src(config.sass.src, { base: '.' })
.pipe(catchDevErrors(sass({ outputStyle: args.target === 'build:release' ? 'compressed' : 'nested' })))
.pipe(dest(args.target === 'build:release' ? 'tmp' : '.'));
};
37 changes: 37 additions & 0 deletions build-steps/js-tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// build-steps/js-tasks.js
import gulp from 'gulp';
const { src, dest } = gulp;
import babel from 'gulp-babel';
import browserify from 'browserify';
import source from 'vinyl-source-stream';

export const babelTask = (config, args) => {
if (typeof config.babel === 'undefined') {
return Promise.resolve();
}
return src(config.babel.src, { base: '.' })
.pipe(babel({
presets: ["@babel/preset-env", "@babel/preset-react"],
}))
.pipe(dest(args.target === 'build:release' ? 'tmp' : '.'));
};

export const browserifyTask = (config) => {
if (typeof config.browserify === 'undefined') {
return Promise.resolve();
}

const runBrowserify = (browserifyConfig) => {
return browserify(browserifyConfig.src)
.bundle()
.on('error', (e) => console.error(e))
.pipe(source(browserifyConfig.fileName))
.pipe(dest('tmp'));
};

if (Array.isArray(config.browserify)) {
return Promise.all(config.browserify.map(runBrowserify));
} else {
return runBrowserify(config.browserify);
}
};
36 changes: 36 additions & 0 deletions build-steps/minify-tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// build-steps/minify-tasks.js
import gulp from 'gulp';
const { src, dest } = gulp;
import gulpif from 'gulp-if';
import rename from 'gulp-rename';
import cssnano from 'gulp-cssnano';
import terser from 'gulp-terser';

export const minifyCss = (config, args) => {
if (!config.css) {
return Promise.resolve();
}
return src(config.css.src, { base: '.' })
.pipe(gulpif(args.target === 'build:release', dest('tmp')))
.pipe(rename({ suffix: '.min' }))
.pipe(cssnano({ zindex: false, reduceIdents: false }))
.pipe(dest(args.target === 'build:release' ? 'tmp' : '.'));
};

export const minifyJs = (config, jsMinSuffix) => {
console.log('Starting JS minification...');

const jsFiles = [
...(config.js && config.js.src ? config.js.src : []),
'tmp/**/*.js'
];

return src(jsFiles, { base: '.' })
.pipe(gulpif(file => !file.path.includes('.min.js'), rename({ suffix: jsMinSuffix })))
.pipe(terser().on('error', (err) => {
console.error('Terser error:', err.toString());
this.emit('end');
}))
.pipe(dest('tmp'))
.on('end', () => console.log('JS minification completed.'));
};
98 changes: 98 additions & 0 deletions build-steps/update-font-awesome.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import fetch from 'node-fetch';
import fs from 'fs/promises';
import path from 'path';
import { src, dest } from 'gulp';
import { deleteAsync } from 'del';
import { promisify } from 'util';
import stream from 'stream';
import { createWriteStream } from 'fs';
import unzipper from 'unzipper';

const pipeline = promisify(stream.pipeline);

export const updateFontAwesome = async (config) => {
if (!(config.fontAwesome && config.fontAwesome.base)) {
console.log('Missing fontAwesome.base config value. Need to know where to write the output file.');
return;
}

const tmpDir = path.join(process.cwd(), 'tmp');
const zipFilePath = path.join(tmpDir, 'fontawesome.zip');
const extractDir = path.join(tmpDir, 'fontawesome-extract');
const fontAwesomeTmpDir = path.join(extractDir, 'Font-Awesome-6.x');

try {
// Create tmp directory
await fs.mkdir(tmpDir, { recursive: true });
await fs.mkdir(extractDir, { recursive: true });

console.log('Downloading Font Awesome...');
const response = await fetch('https://github.com/FortAwesome/Font-Awesome/archive/refs/heads/6.x.zip');

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

await pipeline(response.body, createWriteStream(zipFilePath));

console.log('Verifying downloaded file...');
const stats = await fs.stat(zipFilePath);
if (stats.size === 0) {
throw new Error('Downloaded file is empty');
}

console.log('Unzipping Font Awesome...');
await fs.createReadStream(zipFilePath)
.pipe(unzipper.Extract({ path: extractDir }))
.promise();

console.log('Parsing Font Awesome metadata...');
const fontAwesome = JSON.parse(await fs.readFile(path.join(fontAwesomeTmpDir, 'metadata', 'icons.json'), 'utf8'));

let fontsString = '<?php\n\nfunction siteorigin_widgets_icons_fontawesome_filter( $icons ){\n\treturn array_merge($icons, array(\n';
for (const [icon, iconProps] of Object.entries(fontAwesome)) {
fontsString += `\t\t'${icon}' => array( 'unicode' => '&#x${iconProps.unicode};', 'styles' => array( `;
iconProps.styles.forEach((style, i) => {
fontsString += `${i > 0 ? ", " : ""}'sow-fa${style.charAt(0)}'`;
});
fontsString += ' ), ),\n';
}
fontsString += '\n\t));\n}\nadd_filter';

console.log('Updating Font Awesome filter...');
const filterPath = path.join(config.fontAwesome.base, 'filter.php');
const filterData = await fs.readFile(filterPath, 'utf8');
const editedFile = filterData.replace(/<\?php([\s\S]*?)add_filter/, fontsString);
await fs.writeFile(filterPath, editedFile);

console.log('Successfully updated Font Awesome filter.php. Please manually add migration code for any removed icons. https://fontawesome.com/docs/changelog/');

console.log('Updating Font Awesome version...');
const regularCssPath = path.join(fontAwesomeTmpDir, 'css', 'regular.css');
const regularCssData = await fs.readFile(regularCssPath, 'utf8');
const newVersion = regularCssData.match(/Free ([\S]*?) by/);
const stylePath = path.join(config.fontAwesome.base, 'style.css');
const styleData = await fs.readFile(stylePath, 'utf8');
const oldVersion = styleData.match(/Free ([\S]*?) by/);
const newStyle = styleData.replace(oldVersion[1], newVersion[1]);
await fs.writeFile(stylePath, newStyle);

console.log(`Updating Font Awesome ${oldVersion[1]} to ${newVersion[1]}`);

console.log('Moving Font Awesome font files...');
await new Promise((resolve, reject) => {
src(path.join(fontAwesomeTmpDir, 'webfonts', '*'))
.pipe(dest(path.join(config.fontAwesome.base, 'webfonts')))
.on('end', resolve)
.on('error', reject);
});

console.log('Successfully moved Font Awesome font files');
console.log('Font Awesome update completed successfully.');
} catch (error) {
console.error('An error occurred while updating Font Awesome:', error);
} finally {
// Clean up temporary files
await deleteAsync([tmpDir]);
}
};
44 changes: 44 additions & 0 deletions build-steps/update-google-fonts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fetch from 'node-fetch';
import fs from 'fs/promises';

export const updateGoogleFonts = async (config, apiKey) => {
if (!(config.googleFonts && config.googleFonts.dest)) {
console.log('Missing googleFonts.dest config value. Need to know where to write the output file.');
return;
}
if (!apiKey) {
console.log('Missing apiKey argument. Google Fonts requires an API Key.');
return;
}

const outFile = config.googleFonts.dest;
const fontsUrl = `https://www.googleapis.com/webfonts/v1/webfonts?sort=alpha&key=${apiKey}`;

try {
const response = await fetch(fontsUrl);
const body = await response.json();

if (response.status !== 200) {
console.log('An error occurred while fetching fonts:');
console.log(`${body.error.code} ${body.error.message}`);
body.error.errors.forEach(error => console.log(error));
throw new Error('Google Fonts API error');
}

let fontsString = '<?php\n\nreturn array(\n';
body.items.forEach(font => {
fontsString += `\t'${font.family}' =>\n\t\tarray(\n`;
font.variants.forEach((variant, i) => {
fontsString += `\t\t\t${i} => '${variant}',\n`;
});
fontsString += '\t\t),\n';
});
fontsString += ');';

await fs.writeFile(outFile, fontsString);
console.log('Successfully updated Google Fonts.');
} catch (error) {
console.log('An error occurred while fetching fonts:');
console.log(error.message);
}
};
Loading