diff --git a/.changeset/purple-chefs-yell.md b/.changeset/purple-chefs-yell.md
new file mode 100644
index 000000000..8167d6d31
--- /dev/null
+++ b/.changeset/purple-chefs-yell.md
@@ -0,0 +1,5 @@
+---
+'preact-cli': patch
+---
+
+Corrects 'build --json' ouput location and 'apple-touch-icon' will respect the publicPath automatically
diff --git a/packages/cli/lib/commands/build.js b/packages/cli/lib/commands/build.js
index 2b965e552..411b39baa 100644
--- a/packages/cli/lib/commands/build.js
+++ b/packages/cli/lib/commands/build.js
@@ -107,7 +107,7 @@ async function command(src, argv) {
let stats = await runWebpack(argv, false);
if (argv.json) {
- await runWebpack.writeJsonStats(stats);
+ await runWebpack.writeJsonStats(cwd, stats);
}
}
diff --git a/packages/cli/lib/lib/webpack/run-webpack.js b/packages/cli/lib/lib/webpack/run-webpack.js
index c82de0d72..abc1544f5 100644
--- a/packages/cli/lib/lib/webpack/run-webpack.js
+++ b/packages/cli/lib/lib/webpack/run-webpack.js
@@ -122,8 +122,8 @@ function showStats(stats, isProd) {
return stats;
}
-function writeJsonStats(stats) {
- let outputPath = resolve(process.cwd(), 'stats.json');
+function writeJsonStats(cwd, stats) {
+ let outputPath = resolve(cwd, 'stats.json');
let jsonStats = stats.toJson({
json: true,
chunkModules: true,
diff --git a/packages/cli/lib/resources/template.html b/packages/cli/lib/resources/template.html
index 770c48b2b..4b33acd6f 100644
--- a/packages/cli/lib/resources/template.html
+++ b/packages/cli/lib/resources/template.html
@@ -6,7 +6,7 @@
-
+
<% preact.headEnd %>
diff --git a/packages/cli/tests/build.test.js b/packages/cli/tests/build.test.js
index fe172d934..a2be5370b 100644
--- a/packages/cli/tests/build.test.js
+++ b/packages/cli/tests/build.test.js
@@ -1,5 +1,6 @@
const { join } = require('path');
-const { access, readdir, readFile, writeFile } = require('fs').promises;
+const { access, mkdir, readdir, readFile, rename, unlink, writeFile } =
+ require('fs').promises;
const looksLike = require('html-looks-like');
const { create, build } = require('./lib/cli');
const { snapshot } = require('./lib/utils');
@@ -46,28 +47,40 @@ function testMatch(received, expected) {
}
}
+/**
+ * Get build output file as utf-8 string
+ * @param {string} dir
+ * @param {RegExp | string} file
+ * @returns {Promise}
+ */
+async function getOutputFile(dir, file) {
+ if (typeof file !== 'string') {
+ // @ts-ignore
+ file = (await readdir(join(dir, 'build'))).find(f => file.test(f));
+ }
+ return await readFile(join(dir, 'build', file), 'utf8');
+}
+
describe('preact build', () => {
- it(`builds the 'default' template`, async () => {
+ it('builds the `default` template', async () => {
let dir = await create('default');
await build(dir);
- dir = join(dir, 'build');
- let output = await snapshot(dir);
+ let output = await snapshot(join(dir, 'build'));
testMatch(output, images.default);
});
- it(`builds the 'default' template with esm`, async () => {
+ it('builds the `default` template with esm', async () => {
let dir = await create('default');
await build(dir, { esm: true });
- dir = join(dir, 'build');
- let output = await snapshot(dir);
+ let output = await snapshot(join(dir, 'build'));
testMatch(output, images['default-esm']);
});
- it(`builds the 'typescript' template`, async () => {
+ it('builds the `typescript` template', async () => {
let dir = await create('typescript');
// The tsconfig.json in the template covers the test directory,
@@ -77,157 +90,13 @@ describe('preact build', () => {
// Remove when https://github.com/preactjs/enzyme-adapter-preact-pure/issues/161 is resolved
shell.exec('rm tsconfig.json');
- expect(() => build(dir)).not.toThrow();
- });
-
- it('should use SASS styles', async () => {
- let dir = await subject('sass');
- await build(dir);
-
- let body = await getBody(dir);
- looksLike(body, images.sass);
- });
-
- it('should use custom `.babelrc`', async () => {
- // app with custom .babelrc enabling async functions
- let dir = await subject('custom-babelrc');
-
- await build(dir);
-
- const bundleFile = (await readdir(`${dir}/build`)).find(file =>
- /bundle\.\w{5}\.js$/.test(file)
- );
- const transpiledChunk = await readFile(
- `${dir}/build/${bundleFile}`,
- 'utf8'
- );
-
- // when tragetting only last 1 chrome version, babel preserves
- // arrow function. So checking for the delay function code from delay function in
- // https://github.com/preactjs/preact-cli/blob/master/packages/cli/tests/subjects/custom-babelrc/index.js
- expect(transpiledChunk.includes('=>setTimeout')).toBe(true);
- });
-
- prerenderUrlFiles.forEach(prerenderUrls => {
- it(`should prerender the routes provided with '${prerenderUrls}'`, async () => {
- let dir = await subject('multiple-prerendering');
- await build(dir, { prerenderUrls });
-
- const body1 = await getBody(dir);
- looksLike(body1, images.prerender.home);
-
- const body2 = await getBody(dir, 'route66/index.html');
- looksLike(body2, images.prerender.route);
-
- const body3 = await getBody(dir, 'custom/index.html');
- looksLike(body3, images.prerender.custom);
-
- const head1 = await getHead(dir);
- expect(head1).toEqual(
- expect.stringMatching(getRegExpFromMarkup(images.prerender.heads.home))
- );
-
- const head2 = await getHead(dir, 'route66/index.html');
- expect(head2).toEqual(
- expect.stringMatching(
- getRegExpFromMarkup(images.prerender.heads.route66)
- )
- );
-
- const head3 = await getHead(dir, 'custom/index.html');
- expect(head3).toEqual(
- expect.stringMatching(
- getRegExpFromMarkup(images.prerender.heads.custom)
- )
- );
- });
- });
-
- prerenderUrlFiles.forEach(prerenderUrls => {
- it(`should prerender the routes with data provided with '${prerenderUrls}' via provider`, async () => {
- let dir = await subject('multiple-prerendering-with-provider');
- await build(dir, { prerenderUrls });
-
- const body1 = await getBody(dir);
- looksLike(body1, images.prerender.home);
-
- const body2 = await getBody(dir, 'route66/index.html');
- looksLike(body2, images.prerender.route);
-
- const body3 = await getBody(dir, 'custom/index.html');
- looksLike(body3, images.prerender.custom);
-
- const body4 = await getBody(dir, 'customhook/index.html');
- looksLike(body4, images.prerender.customhook);
-
- const body5 = await getBody(dir, 'htmlsafe/index.html');
- looksLike(body5, images.prerender.htmlSafe);
-
- const head1 = await getHead(dir);
- expect(head1).toEqual(
- expect.stringMatching(getRegExpFromMarkup(images.prerender.heads.home))
- );
-
- const head2 = await getHead(dir, 'route66/index.html');
- expect(head2).toEqual(
- expect.stringMatching(
- getRegExpFromMarkup(images.prerender.heads.route66)
- )
- );
-
- const head3 = await getHead(dir, 'custom/index.html');
- expect(head3).toEqual(
- expect.stringMatching(
- getRegExpFromMarkup(images.prerender.heads.custom)
- )
- );
- });
- });
-
- it('should preload correct files', async () => {
- let dir = await subject('preload-chunks');
- await build(dir, { preload: true });
-
- const head1 = await getHead(dir);
- expect(head1).toEqual(
- expect.stringMatching(getRegExpFromMarkup(images.preload.head))
- );
- });
-
- it('should use custom `preact.config.js`', async () => {
- // app with stable output name via preact.config.js
- let dir = await subject('custom-webpack');
- await build(dir);
-
- let stableOutput = join(dir, 'build/bundle.js');
- expect(await access(stableOutput)).toBeUndefined();
- });
-
- it('should use custom `template.html`', async () => {
- let dir = await subject('custom-template');
- await build(dir);
-
- let file = join(dir, 'build/index.html');
- let html = await readFile(file, 'utf-8');
-
- expect(html).toEqual(
- expect.stringMatching(getRegExpFromMarkup(images.template))
- );
+ await expect(build(dir)).resolves.not.toThrow();
});
it('should patch global location object', async () => {
let dir = await subject('location-patch');
- expect(() => build(dir)).not.toThrow();
- });
- it('should import non-modules CSS even when side effects are false', async () => {
- let dir = await subject('side-effect-css');
- await build(dir);
-
- let head = await getHead(dir);
- expect(head).toEqual(
- expect.stringMatching(getRegExpFromMarkup(images.sideEffectCss))
- );
+ await expect(build(dir)).resolves.not.toThrow();
});
it('should copy resources from static to build directory', async () => {
@@ -237,16 +106,6 @@ describe('preact build', () => {
expect(await access(file)).toBeUndefined();
});
- it('should error out for invalid CLI argument', async () => {
- let dir = await subject('custom-template');
- const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
- await expect(build(dir, { 'service-worker': false })).rejects.toEqual(
- new Error('Invalid argument found.')
- );
- expect(mockExit).toHaveBeenCalledWith(1);
- mockExit.mockRestore();
- });
-
describe('Push manifest plugin', () => {
it('should produce correct default `push-manifest.json`', async () => {
let dir = await create('default');
@@ -326,4 +185,328 @@ describe('preact build', () => {
// "Hello World!" should replace 'process.env.PREACT_APP_MY_VARIABLE'
expect(transpiledChunk.includes('console.log("Hello World!")')).toBe(true);
});
+
+ it('should respect `publicPath` value', async () => {
+ let dir = await subject('public-path');
+ await build(dir);
+ const html = await getOutputFile(dir, 'index.html');
+
+ expect(html).toEqual(
+ expect.stringMatching(getRegExpFromMarkup(images.publicPath))
+ );
+ });
+
+ describe('CLI Options', () => {
+ it('--src', async () => {
+ let dir = await subject('minimal');
+
+ await mkdir(join(dir, 'renamed-src'));
+ await rename(join(dir, 'index.js'), join(dir, 'renamed-src/index.js'));
+ await rename(join(dir, 'style.css'), join(dir, 'renamed-src/style.css'));
+
+ await expect(build(dir, { src: 'renamed-src' })).resolves.toBeUndefined();
+ });
+
+ it('--dest', async () => {
+ let dir = await subject('minimal');
+
+ await build(dir, { dest: 'renamed-dest' });
+ expect(await access(join(dir, 'renamed-dest'))).toBeUndefined();
+ });
+
+ it('--sw', async () => {
+ let dir = await subject('minimal');
+
+ const logSpy = jest.spyOn(process.stdout, 'write');
+
+ await build(dir, { sw: true });
+ expect(await access(join(dir, 'build', 'sw.js'))).toBeUndefined();
+ expect(logSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Could not find sw.js')
+ );
+
+ await build(dir, { sw: false });
+ await expect(access(join(dir, 'build', 'sw.js'))).rejects.toThrow(
+ 'no such file or directory'
+ );
+ });
+
+ it('--babelConfig', async () => {
+ let dir = await subject('custom-babelrc');
+
+ await build(dir);
+ let transpiledChunk = await getOutputFile(dir, /bundle\.\w{5}\.js$/);
+ expect(/=>\s?setTimeout/.test(transpiledChunk)).toBe(true);
+
+ await rename(join(dir, '.babelrc'), join(dir, 'babel.config.json'));
+ await build(dir, {
+ babelConfig: 'babel.config.json',
+ });
+ transpiledChunk = await getOutputFile(dir, /bundle\.\w{5}\.js$/);
+ expect(/=>\s?setTimeout/.test(transpiledChunk)).toBe(true);
+ });
+
+ it('--json', async () => {
+ let dir = await subject('minimal');
+
+ await build(dir, { json: true });
+ expect(await access(join(dir, 'stats.json'))).toBeUndefined();
+ // Need to clean up manually as it is placed in project root
+ await unlink(join(dir, 'stats.json'));
+
+ await build(dir, { json: false });
+ await expect(access(join(dir, 'stats.json'))).rejects.toThrow(
+ 'no such file or directory'
+ );
+ });
+
+ it('--template', async () => {
+ let dir = await subject('custom-template');
+
+ await rename(
+ join(dir, 'template.html'),
+ join(dir, 'renamed-template.html')
+ );
+ await build(dir, { template: 'renamed-template.html' });
+
+ const html = await getOutputFile(dir, 'index.html');
+ expect(html).toEqual(
+ expect.stringMatching(getRegExpFromMarkup(images.template))
+ );
+ });
+
+ it('--preload', async () => {
+ let dir = await subject('preload-chunks');
+
+ await build(dir, { preload: true });
+ let head = await getHead(dir);
+ expect(head).toEqual(
+ expect.stringMatching(getRegExpFromMarkup(images.preload.true))
+ );
+
+ await build(dir, { preload: false });
+ head = await getHead(dir);
+ expect(head).toEqual(
+ expect.stringMatching(getRegExpFromMarkup(images.preload.false))
+ );
+ });
+
+ it('--prerender', async () => {
+ let dir = await subject('minimal');
+
+ await build(dir, { prerender: true });
+ let html = await getOutputFile(dir, 'index.html');
+ expect(html).toMatch('Minimal App
');
+
+ await build(dir, { prerender: false });
+ html = await getOutputFile(dir, 'index.html');
+ expect(html).not.toMatch('Minimal App
');
+ });
+
+ it('--prerenderUrls', async () => {
+ let dir = await subject('multiple-prerendering');
+
+ await build(dir, { prerenderUrls: 'prerender-urls.json' });
+ expect(await access(join(dir, 'build/index.html'))).toBeUndefined();
+ expect(
+ await access(join(dir, 'build/route66/index.html'))
+ ).toBeUndefined();
+ expect(
+ await access(join(dir, 'build/custom/index.html'))
+ ).toBeUndefined();
+
+ await rename(
+ join(dir, 'prerender-urls.json'),
+ join(dir, 'renamed-urls.json')
+ );
+ await build(dir, { prerenderUrls: 'renamed-urls.json' });
+ expect(await access(join(dir, 'build/index.html'))).toBeUndefined();
+ expect(
+ await access(join(dir, 'build/route66/index.html'))
+ ).toBeUndefined();
+ expect(
+ await access(join(dir, 'build/custom/index.html'))
+ ).toBeUndefined();
+ });
+
+ it('--inline-css', async () => {
+ let dir = await subject('minimal');
+
+ await build(dir, { 'inline-css': true });
+ let head = await getHead(dir);
+ expect(head).toMatch('');
+
+ await build(dir, { 'inline-css': false });
+ head = await getOutputFile(dir, 'index.html');
+ expect(head).not.toMatch(/');
+ });
+
+ // Issue #1411
+ it('should preserve side-effectful CSS imports even if package.json claims no side effects', async () => {
+ let dir = await subject('css-side-effect');
+ await build(dir);
+
+ const builtStylesheet = await getOutputFile(dir, /bundle\.\w{5}\.css$/);
+ expect(builtStylesheet).toMatch('h1{background:#673ab8}');
+ });
+
+ it('should use SASS styles', async () => {
+ let dir = await subject('css-sass');
+ await build(dir);
+
+ let body = await getBody(dir);
+ looksLike(body, images.sass);
+ });
+ });
+
+ describe('prerender', () => {
+ prerenderUrlFiles.forEach(prerenderUrls => {
+ it(`should prerender the routes provided with '${prerenderUrls}'`, async () => {
+ let dir = await subject('multiple-prerendering');
+ await build(dir, { prerenderUrls });
+
+ const body1 = await getBody(dir);
+ looksLike(body1, images.prerender.home);
+
+ const body2 = await getBody(dir, 'route66/index.html');
+ looksLike(body2, images.prerender.route);
+
+ const body3 = await getBody(dir, 'custom/index.html');
+ looksLike(body3, images.prerender.custom);
+
+ const head1 = await getHead(dir);
+ expect(head1).toEqual(
+ expect.stringMatching(
+ getRegExpFromMarkup(images.prerender.heads.home)
+ )
+ );
+
+ const head2 = await getHead(dir, 'route66/index.html');
+ expect(head2).toEqual(
+ expect.stringMatching(
+ getRegExpFromMarkup(images.prerender.heads.route66)
+ )
+ );
+
+ const head3 = await getHead(dir, 'custom/index.html');
+ expect(head3).toEqual(
+ expect.stringMatching(
+ getRegExpFromMarkup(images.prerender.heads.custom)
+ )
+ );
+ });
+ });
+
+ prerenderUrlFiles.forEach(prerenderUrls => {
+ it(`should prerender the routes with data provided with '${prerenderUrls}' via provider`, async () => {
+ let dir = await subject('multiple-prerendering-with-provider');
+ await build(dir, { prerenderUrls });
+
+ const body1 = await getBody(dir);
+ looksLike(body1, images.prerender.home);
+
+ const body2 = await getBody(dir, 'route66/index.html');
+ looksLike(body2, images.prerender.route);
+
+ const body3 = await getBody(dir, 'custom/index.html');
+ looksLike(body3, images.prerender.custom);
+
+ const body4 = await getBody(dir, 'customhook/index.html');
+ looksLike(body4, images.prerender.customhook);
+
+ const body5 = await getBody(dir, 'htmlsafe/index.html');
+ looksLike(body5, images.prerender.htmlSafe);
+
+ const head1 = await getHead(dir);
+ expect(head1).toEqual(
+ expect.stringMatching(
+ getRegExpFromMarkup(images.prerender.heads.home)
+ )
+ );
+
+ const head2 = await getHead(dir, 'route66/index.html');
+ expect(head2).toEqual(
+ expect.stringMatching(
+ getRegExpFromMarkup(images.prerender.heads.route66)
+ )
+ );
+
+ const head3 = await getHead(dir, 'custom/index.html');
+ expect(head3).toEqual(
+ expect.stringMatching(
+ getRegExpFromMarkup(images.prerender.heads.custom)
+ )
+ );
+ });
+ });
+ });
});
diff --git a/packages/cli/tests/create.test.js b/packages/cli/tests/create.test.js
index 0c13dae8c..35dcece05 100644
--- a/packages/cli/tests/create.test.js
+++ b/packages/cli/tests/create.test.js
@@ -1,4 +1,4 @@
-const { readFileSync } = require('fs');
+const { readFile } = require('fs').promises;
const { relative, resolve } = require('path');
const { create } = require('./lib/cli');
const { expand } = require('./lib/utils');
@@ -19,7 +19,7 @@ describe('preact create', () => {
let dir = await create('netlify');
const templateFilePath = resolve(__dirname, dir, 'src', 'template.html');
- const template = readFileSync(templateFilePath).toString('utf8');
+ const template = await readFile(templateFilePath, 'utf8');
expect(template.includes('twitter:card')).toEqual(true);
});
@@ -28,12 +28,13 @@ describe('preact create', () => {
let dir = await create('simple');
const templateFilePath = resolve(__dirname, dir, 'src', 'template.html');
- const template = readFileSync(templateFilePath).toString('utf8');
+ const template = await readFile(templateFilePath, 'utf8');
expect(template.includes('apple-touch-icon')).toEqual(true);
});
it('should fail given an invalid name', async () => {
+ // @ts-ignore
const exit = jest.spyOn(process, 'exit').mockImplementation(() => {});
await create('simple', '*()@!#!$-Invalid-Name');
diff --git a/packages/cli/tests/images/build.js b/packages/cli/tests/images/build.js
index ab4371d7a..6dd3bc987 100644
--- a/packages/cli/tests/images/build.js
+++ b/packages/cli/tests/images/build.js
@@ -57,23 +57,6 @@ exports.sass = `
`;
-exports.sideEffectCss = `
-
-
- side-effect-css<\\/title>
-
-
-
-
-
-
+
+
+ Public path test
+
+
+
+
+