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

refactor: Move EJS into user templates (template.html) #1768

Merged
merged 6 commits into from
Jan 7, 2023
Merged
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
9 changes: 9 additions & 0 deletions .changeset/hungry-peas-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'preact-cli': major
---

To increase transparency and user control over the `template.html`, `<% preact.headEnd %>` and `<% preact.bodyEnd %>` will no longer be supported; instead, users should directly adopt the EJS and keep it in their templates.

In the past, these were abstracted away as they were a bit unwieldy; EJS might be unfamiliar with users and the way data was retrieved from `html-webpack-plugin` was somewhat less than elegant. However, this has much improved over the years and the abstraction only makes simple edits less than obvious, so it is no longer fulfilling it's purpose.

New projects will have a `template.ejs` created in place of the old `template.html`, containing the full EJS template. For existing projects, you can copy [the default `template.ejs`](https://github.com/preactjs/preact-cli/blob/master/packages/cli/src/resources/template.ejs) into your project or adapt it as you wish.
18 changes: 9 additions & 9 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
{
"extends": [
"eslint:recommended",
"prettier"
],
"extends": ["eslint:recommended", "prettier"],
"parser": "babel-eslint",
"env": {
"browser": true,
"node": true,
"es6": true
},
"plugins": [
"babel",
"react",
"prettier"
],
"plugins": ["babel", "react", "prettier"],
"settings": {
"react": {
"pragma": "h",
Expand All @@ -29,6 +22,13 @@
"rules": {
"no-console": 1,
"no-empty": 0,
"no-unused-vars": [
2,
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"semi": 2,
"keyword-spacing": 2,
"require-atomic-updates": 0,
Expand Down
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ $ [npm run / yarn] preact build
--babelConfig Path to custom Babel config (default .babelrc)
--prerender Renders route(s) into generated static HTML (default true)
--prerenderUrls Path to pre-rendered routes config (default prerender-urls.json)
--template Path to custom HTML template (default 'src/template.html')
--template Path to custom EJS or HTML template (default 'src/template.ejs')
--inlineCss Adds critical css to the prerendered markup (default true)
--analyze Launch interactive Analyzer to inspect production bundle(s) (default false)
-c, --config Path to custom CLI config (default preact.config.js)
Expand All @@ -151,7 +151,7 @@ $ [npm run / yarn] preact watch
--cacert Path to optional CA certificate override
--prerender Pre-render static content on first run
--prerenderUrls Path to pre-rendered routes config (default prerender-urls.json)
--template Path to custom HTML template (default 'src/template.html')
--template Path to custom EJS or HTML template (default 'src/template.ejs')
--refresh Enables experimental preact-refresh functionality
-c, --config Path to custom CLI config (default preact.config.js)
-H, --host Set server hostname (default 0.0.0.0)
Expand Down Expand Up @@ -211,7 +211,7 @@ To customize Babel, you have two options:

#### Webpack

To customize preact-cli create a `preact.config.js` or a `preact.config.json` file.
To customize Preact-CLI's Webpack config, create a `preact.config.js` or a `preact.config.json` file:

> `preact.config.js`

Expand Down Expand Up @@ -295,18 +295,15 @@ export default () => {

#### Template

A template is used to render your page by [EJS](https://ejs.co/).
You can uses the data of `prerenderUrls` which does not have `title`, using `htmlWebpackPlugin.options.CLI_DATA.preRenderData` in EJS.
To customize the HTML document that your app uses, edit the `template.ejs` file in your app's source directory.

The default one is visible [here](packages/cli/src/resources/template.html) and it's going to be enough for the majority of cases.
[EJS](https://ejs.dev) is a simple templating language that lets you generate HTML markup with plain JavaScript. Alongside `html-webpack-plugin`, you're able to conditionally add HTML, access your bundles and assets, and link to external content if you wish. The default we provide on project initialization should fit the majority of use cases very well, but feel free to customize!

If you want to customise your template you can pass a custom template with the `--template` flag.

The `--template` flag is available on the `build` and `watch` commands.
You can customize the location of your template with the `--template` flag on the `build` and `watch` commands:

```sh
preact build --template src/template.html
preact watch --template src/template.html
preact build --template renamed-src/template.ejs
preact watch --template template.ejs
```

### Using CSS preprocessors
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ prog
.option('--babelConfig', 'Path to custom Babel config', '.babelrc')
.option(
'--template',
'Path to custom HTML template (default src/template.html)'
'Path to custom EJS or HTML template (default src/template.ejs)'
)
.option(
'--analyze',
Expand Down
119 changes: 77 additions & 42 deletions packages/cli/src/lib/webpack/render-html-plugin.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,82 @@
const { mkdir, readFile, writeFile } = require('fs/promises');
const { existsSync } = require('fs');
const { resolve, join } = require('path');
const os = require('os');
const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('fs');
const { tmpdir } = require('os');
const { Compilation, sources } = require('webpack');
const {
HtmlWebpackSkipAssetsPlugin,
} = require('html-webpack-skip-assets-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const prerender = require('./prerender');
const { esmImport, tryResolveConfig, warn } = require('../../util');
const { esmImport, error, tryResolveConfig, warn } = require('../../util');

const PREACT_FALLBACK_URL = '/200.html';

let defaultTemplate = resolve(__dirname, '../../resources/template.html');

function read(path) {
return readFileSync(resolve(__dirname, path), 'utf-8');
}

/**
* @param {import('../../../types').Env} env
*/
module.exports = async function renderHTMLPlugin(config, env) {
const { cwd, dest, src } = config;

const inProjectTemplatePath = resolve(src, 'template.html');
let template = defaultTemplate;
if (existsSync(inProjectTemplatePath)) {
template = inProjectTemplatePath;
let templatePath;
if (config.template) {
templatePath = tryResolveConfig(
cwd,
config.template,
false,
config.verbose
);
}

if (config.template) {
const templatePathFromArg = resolve(cwd, config.template);
if (existsSync(templatePathFromArg)) template = templatePathFromArg;
else {
warn(`Template not found at ${templatePathFromArg}`);
if (!templatePath) {
templatePath = tryResolveConfig(
cwd,
resolve(src, 'template.ejs'),
true,
config.verbose
);

// Additionally checks for <src-dir>/template.html for
// back-compat with v3
if (!templatePath) {
templatePath = tryResolveConfig(
cwd,
resolve(src, 'template.html'),
true,
config.verbose
);
}

if (!templatePath)
templatePath = resolve(__dirname, '../../resources/template.ejs');
}

let content = read(template);
if (/preact\.(title|headEnd|bodyEnd)/.test(content)) {
const headEnd = read('../../resources/head-end.ejs');
const bodyEnd = read('../../resources/body-end.ejs');
content = content
.replace(/<%[=]?\s+preact\.title\s+%>/, '<%= cli.title %>')
.replace(/<%\s+preact\.headEnd\s+%>/, headEnd)
.replace(/<%\s+preact\.bodyEnd\s+%>/, bodyEnd);
let templateContent = await readFile(templatePath, 'utf-8');
if (/preact\.(?:headEnd|bodyEnd)/.test(templateContent)) {
const message = `
'<% preact.headEnd %>' and '<% preact.bodyEnd %>' are no longer supported in CLI v4!
You can copy the new default 'template.ejs' from the following link or adapt your existing:

https://github.com/preactjs/preact-cli/blob/master/packages/cli/src/resources/template.ejs
`;

error(message.trim().replace(/^\t+/gm, '') + '\n');
}
if (/preact\.title/.test(templateContent)) {
templateContent = templateContent.replace(
/<%[=]?\s+preact\.title\s+%>/,
'<%= cli.title %>'
);

// Unfortunately html-webpack-plugin expects a true file,
// so we'll create a temporary one.
const tmpDir = join(
os.tmpdir(),
tmpdir(),
`preact-cli-${Math.floor(Math.random() * 100000)}`
);
if (!existsSync(tmpDir)) {
mkdirSync(tmpDir);
}
template = resolve(tmpDir, 'template.tmp.ejs');
writeFileSync(template, content);
await mkdir(tmpDir);
templatePath = resolve(tmpDir, 'template.tmp.ejs');
writeFile(templatePath, templateContent);
}

const htmlWebpackConfig = values => {
Expand All @@ -78,29 +98,46 @@ module.exports = async function renderHTMLPlugin(config, env) {
return {
title,
filename,
template: `!!${require.resolve('ejs-loader')}?esModule=false!${template}`,
template: `!!${require.resolve(
'ejs-loader'
)}?esModule=false!${templatePath}`,
templateParameters: async (compilation, assets, assetTags, options) => {
let entrypoints = {};
compilation.entrypoints.forEach((entrypoint, name) => {
let entryFiles = entrypoint.getFiles();

entrypoints[name] =
assets.publicPath +
entryFiles.find(file => /\.(m?js)(\?|$)/.test(file));
entryFiles.find(file => /\.m?js(?:\?|$)/.test(file));
});

const compilationAssets = Object.keys(compilation.assets);
const outputName = entrypoints['bundle']
.match(/^([^.]*)/)[1]
.replace(assets.publicPath, '');
const rgx = new RegExp(`${outputName}.*legacy.js$`);
const legacyOutput = compilationAssets.find(file => rgx.test(file));
if (legacyOutput) {
entrypoints['legacy-bundle'] = assets.publicPath + legacyOutput;
}

if (compilationAssets.includes('es-polyfills.js')) {
entrypoints['es-polyfills'] = assets.publicPath + 'es-polyfills.js';
}

return {
cli: {
title,
url,
manifest: config.manifest,
inlineCss: config['inlineCss'],
Copy link
Member Author

Choose a reason for hiding this comment

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

Already available as config.inlineCss, no point in having a duplicated top level key for it.

config,
// pkg isn't likely to be useful and manifest is already made available
config: (({ pkg: _pkg, manifest: _manifest, ...rest }) => rest)(
config
),
env,
preRenderData: values,
CLI_DATA: { preRenderData: { url, ...routeData } },
ssr: config.prerender
? await prerender(config, values)
: '',
ssr: config.prerender ? await prerender(config, values) : '',
entrypoints,
},
htmlWebpackPlugin: {
Expand Down Expand Up @@ -201,7 +238,7 @@ class PrerenderDataExtractPlugin {
}
let path = this.location_ + 'preact_prerender_data.json';
if (path.startsWith('/')) {
path = path.substr(1);
path = path.substring(1);
}
compilation.emitAsset(path, new sources.RawSource(this.data_));
}
Expand All @@ -210,5 +247,3 @@ class PrerenderDataExtractPlugin {
);
}
}

exports.PREACT_FALLBACK_URL = PREACT_FALLBACK_URL;
14 changes: 0 additions & 14 deletions packages/cli/src/resources/body-end.ejs

This file was deleted.

4 changes: 0 additions & 4 deletions packages/cli/src/resources/head-end.ejs

This file was deleted.

41 changes: 0 additions & 41 deletions packages/cli/src/resources/static-app.json

This file was deleted.

27 changes: 27 additions & 0 deletions packages/cli/src/resources/template.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><% preact.title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="<%= htmlWebpackPlugin.files.publicPath %>assets/icons/apple-touch-icon.png">
<link rel="manifest" href="<%= htmlWebpackPlugin.files.publicPath %>manifest.json">
<% if (cli.manifest.theme_color) { %>
<meta name="theme-color" content="<%= cli.manifest.theme_color %>">
<% } %>
</head>
<body>
<%= cli.ssr %>
<% if (cli.config.prerender === true) { %>
<script type="__PREACT_CLI_DATA__">
<%= encodeURI(JSON.stringify(cli.CLI_DATA)) %>
</script>
<% } %>
<script type="module" src="<%= cli.entrypoints['bundle'] %>"></script>
<script nomodule src="<%= cli.entrypoints['dom-polyfills'] %>"></script>
<script nomodule src="<%= cli.entrypoints['es-polyfills'] %>"></script>
<script nomodule defer src="<%= cli.entrypoints['legacy-bundle'] %>"></script>
</body>
</html>
15 changes: 0 additions & 15 deletions packages/cli/src/resources/template.html

This file was deleted.

Loading