diff --git a/.changeset/hungry-peas-look.md b/.changeset/hungry-peas-look.md new file mode 100644 index 000000000..6580e5585 --- /dev/null +++ b/.changeset/hungry-peas-look.md @@ -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. diff --git a/.eslintrc b/.eslintrc index 78212fde0..fbb76696d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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", @@ -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, diff --git a/README.md b/README.md index 8a57f2dab..35ce9d9a1 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) @@ -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` @@ -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 diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index fc4269be8..4de77535c 100755 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -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', diff --git a/packages/cli/src/lib/webpack/render-html-plugin.js b/packages/cli/src/lib/webpack/render-html-plugin.js index b9990113d..65f322ba2 100644 --- a/packages/cli/src/lib/webpack/render-html-plugin.js +++ b/packages/cli/src/lib/webpack/render-html-plugin.js @@ -1,56 +1,78 @@ +const { mkdtemp, readFile, writeFile } = require('fs/promises'); +const { existsSync } = require('fs'); const { resolve, join } = require('path'); const os = require('os'); -const { existsSync, mkdtempSync, readFileSync, writeFileSync } = require('fs'); 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 /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 = mkdtempSync(join(os.tmpdir(), 'preact-cli-')); - template = resolve(tmpDir, 'template.tmp.ejs'); - writeFileSync(template, content); + const tmpDir = await mkdtemp(join(os.tmpdir(), 'preact-cli-')); + templatePath = resolve(tmpDir, 'template.tmp.ejs'); + await writeFile(templatePath, templateContent); } const htmlWebpackConfig = values => { @@ -72,29 +94,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'], - 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: { @@ -195,7 +234,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_)); } @@ -204,5 +243,3 @@ class PrerenderDataExtractPlugin { ); } } - -exports.PREACT_FALLBACK_URL = PREACT_FALLBACK_URL; diff --git a/packages/cli/src/resources/body-end.ejs b/packages/cli/src/resources/body-end.ejs deleted file mode 100644 index 96808477f..000000000 --- a/packages/cli/src/resources/body-end.ejs +++ /dev/null @@ -1,14 +0,0 @@ -<%= cli.ssr %> -<% if (cli.config.prerender === true) { %> - -<% } %> - -<% /* - Fetch and Promise polyfills are not needed for browsers that support type=module - Please re-evaluate below line if adding more polyfills. -*/ %> - - - diff --git a/packages/cli/src/resources/head-end.ejs b/packages/cli/src/resources/head-end.ejs deleted file mode 100644 index 63cf7b89c..000000000 --- a/packages/cli/src/resources/head-end.ejs +++ /dev/null @@ -1,4 +0,0 @@ - -<% if (cli.manifest.theme_color) { %> - -<% } %> diff --git a/packages/cli/src/resources/static-app.json b/packages/cli/src/resources/static-app.json deleted file mode 100644 index a8d422cc6..000000000 --- a/packages/cli/src/resources/static-app.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ], - "headers": [ - { - "source": "**", - "headers": [ - { - "key": "Cache-Control", - "value": "public, max-age=3600, no-cache" - }, - { - "key": "Access-Control-Max-Age", - "value": "600" - } - ] - }, - { - "source": "/sw.js", - "headers": [ - { - "key": "Cache-Control", - "value": "private, no-cache" - } - ] - }, - { - "source": "**/*.chunk.*.js", - "headers": [ - { - "key": "Cache-Control", - "value": "public, max-age=31536000" - } - ] - } - ] -} diff --git a/packages/cli/src/resources/template.ejs b/packages/cli/src/resources/template.ejs new file mode 100644 index 000000000..fce91cd13 --- /dev/null +++ b/packages/cli/src/resources/template.ejs @@ -0,0 +1,27 @@ + + + + + <% preact.title %> + + + + + + <% if (cli.manifest.theme_color) { %> + + <% } %> + + + <%= cli.ssr %> + <% if (cli.config.prerender === true) { %> + + <% } %> + + + + + + diff --git a/packages/cli/src/resources/template.html b/packages/cli/src/resources/template.html deleted file mode 100644 index 4b33acd6f..000000000 --- a/packages/cli/src/resources/template.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - <% preact.title %> - - - - - <% preact.headEnd %> - - - <% preact.bodyEnd %> - - diff --git a/packages/cli/tests/build.test.js b/packages/cli/tests/build.test.js index a4a3acbbe..3fd72fdef 100644 --- a/packages/cli/tests/build.test.js +++ b/packages/cli/tests/build.test.js @@ -115,7 +115,7 @@ describe('preact build', () => { it('should respect `publicPath` value', async () => { let dir = await subject('public-path'); - await buildFast(dir); + await build(dir); const html = await getOutputFile(dir, 'index.html'); expect(html).toEqual( @@ -182,10 +182,10 @@ describe('preact build', () => { let dir = await subject('custom-template'); await rename( - join(dir, 'template.html'), - join(dir, 'renamed-template.html') + join(dir, 'template.ejs'), + join(dir, 'renamed-template.ejs') ); - await buildFast(dir, { template: 'renamed-template.html' }); + await buildFast(dir, { template: 'renamed-template.ejs' }); const html = await getOutputFile(dir, 'index.html'); expect(html).toEqual( diff --git a/packages/cli/tests/client.test.js b/packages/cli/tests/client.test.js index 664c06ae0..cf519fd9b 100644 --- a/packages/cli/tests/client.test.js +++ b/packages/cli/tests/client.test.js @@ -9,7 +9,7 @@ const getPort = require('get-port'); let chrome; let PORT; -describe('client-side tests', () => { +describe('client', () => { beforeAll(async () => { chrome = await startChrome(); }); diff --git a/packages/cli/tests/images/build.js b/packages/cli/tests/images/build.js index d3518676a..d61fe29ad 100644 --- a/packages/cli/tests/images/build.js +++ b/packages/cli/tests/images/build.js @@ -151,18 +151,11 @@ exports.template = ` preact-custom-template -

Guess what

This is an app with custom template

-

This is an app with custom template

- - - - - `; @@ -183,7 +176,7 @@ exports.publicPath = `

Public path test

- + diff --git a/packages/cli/tests/service-worker.test.js b/packages/cli/tests/service-worker.test.js index ed894d810..c77d880a0 100644 --- a/packages/cli/tests/service-worker.test.js +++ b/packages/cli/tests/service-worker.test.js @@ -21,7 +21,7 @@ async function enableOfflineMode(page, browser) { }); } -describe('preact service worker tests', () => { +describe('service worker', () => { let server, browser, dir; beforeAll(async () => { diff --git a/packages/cli/tests/subjects/custom-template/template.html b/packages/cli/tests/subjects/custom-template/template.ejs similarity index 89% rename from packages/cli/tests/subjects/custom-template/template.html rename to packages/cli/tests/subjects/custom-template/template.ejs index 8ad6c331f..7605aa797 100644 --- a/packages/cli/tests/subjects/custom-template/template.html +++ b/packages/cli/tests/subjects/custom-template/template.ejs @@ -8,12 +8,10 @@ <% } else { %> <% } %> - <% preact.headEnd %>

Guess what

<%= cli.ssr %> - <% preact.bodyEnd %> diff --git a/packages/cli/tests/watch.test.js b/packages/cli/tests/watch.test.js index a124178dd..65a7467ba 100644 --- a/packages/cli/tests/watch.test.js +++ b/packages/cli/tests/watch.test.js @@ -11,7 +11,7 @@ const fetch = require('isomorphic-unfetch'); const { loadPage, waitUntilExpression } = startChrome; let chrome, server; -describe('preact', () => { +describe('preact watch', () => { beforeAll(async () => { chrome = await startChrome(); }); @@ -176,13 +176,13 @@ describe('preact', () => { let app = await subject('custom-template'); await rename( - join(app, 'template.html'), - join(app, 'renamed-template.html') + join(app, 'template.ejs'), + join(app, 'renamed-template.ejs') ); server = await watch(app, { port: 8095, - template: 'renamed-template.html', + template: 'renamed-template.ejs', }); const html = await fetch('http://127.0.0.1:8095/').then(res => res.text() diff --git a/packages/create-cli/src/commands/create.js b/packages/create-cli/src/commands/create.js index 9aa3bbfa3..e8739d4af 100644 --- a/packages/create-cli/src/commands/create.js +++ b/packages/create-cli/src/commands/create.js @@ -97,12 +97,12 @@ exports.create = async function createCommand(repo, dest, argv) { if (!repo.includes('widget')) { const sourceDirectory = join(resolve(cwd, dest), 'src'); - // Copy over template.html + // Copy over template.ejs const templateSrc = resolve( __dirname, - join('..', 'resources', 'template.html') + join('..', 'resources', 'template.ejs') ); - const templateDest = join(sourceDirectory, 'template.html'); + const templateDest = join(sourceDirectory, 'template.ejs'); await copyTemplateFile(templateSrc, templateDest, argv.force); // Copy over sw.js diff --git a/packages/create-cli/src/resources/template.ejs b/packages/create-cli/src/resources/template.ejs new file mode 100644 index 000000000..fce91cd13 --- /dev/null +++ b/packages/create-cli/src/resources/template.ejs @@ -0,0 +1,27 @@ + + + + + <% preact.title %> + + + + + + <% if (cli.manifest.theme_color) { %> + + <% } %> + + + <%= cli.ssr %> + <% if (cli.config.prerender === true) { %> + + <% } %> + + + + + + diff --git a/packages/create-cli/src/resources/template.html b/packages/create-cli/src/resources/template.html deleted file mode 100644 index 4b33acd6f..000000000 --- a/packages/create-cli/src/resources/template.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - <% preact.title %> - - - - - <% preact.headEnd %> - - - <% preact.bodyEnd %> - - diff --git a/packages/create-cli/tests/images/create.js b/packages/create-cli/tests/images/create.js index a168295b7..aafe85de4 100644 --- a/packages/create-cli/tests/images/create.js +++ b/packages/create-cli/tests/images/create.js @@ -22,7 +22,7 @@ exports.default = [ 'src/routes/profile/index.js', 'src/style/index.css', 'src/sw.js', - 'src/template.html', + 'src/template.ejs', 'tests/__mocks__/browserMocks.js', 'tests/__mocks__/fileMocks.js', 'tests/__mocks__/setupTests.js',