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

Fix/prerender urls esm #1667

Merged
merged 9 commits into from Mar 22, 2022
5 changes: 5 additions & 0 deletions .changeset/dirty-socks-invite.md
@@ -0,0 +1,5 @@
---
'preact-cli': patch
---

Allows users to author prerender-urls.js as ESM once again
19 changes: 18 additions & 1 deletion README.md
Expand Up @@ -266,8 +266,9 @@ preact build --prerenderUrls src/prerender-urls.json
If a static JSON file is too restrictive, you may want to provide a javascript file that exports your routes instead.
Routes can be exported as a JSON string or an object and can optionally be returned from a function.

> `prerender-urls.js`

```js
// prerender-urls.js
module.exports = [
{
url: '/',
Expand All @@ -279,6 +280,22 @@ module.exports = [
];
```

> `prerender-urls.js`

```js
export default () => {
return [
{
url: '/',
title: 'Homepage',
},
{
url: '/route/random',
},
];
};
```

#### Template

A template is used to render your page by [EJS](https://ejs.co/).
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/jest.config.js
Expand Up @@ -31,4 +31,7 @@ module.exports = {

// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",

// TODO: Restored in #1667, remove when upgrading Jest
testEnvironment: 'node',
};
32 changes: 13 additions & 19 deletions packages/cli/lib/lib/webpack/render-html-plugin.js
Expand Up @@ -5,8 +5,7 @@ const HtmlWebpackExcludeAssetsPlugin = require('html-webpack-exclude-assets-plug
const HtmlWebpackPlugin = require('html-webpack-plugin');
const prerender = require('./prerender');
const createLoadManifest = require('./create-load-manifest');
const { warn } = require('../../util');
const { info } = require('../../util');
const { esmImport, tryResolveConfig, warn } = require('../../util');

const PREACT_FALLBACK_URL = '/200.html';

Expand Down Expand Up @@ -107,21 +106,27 @@ module.exports = async function (config) {
let pages = [{ url: '/' }];

if (config.prerenderUrls) {
if (existsSync(resolve(cwd, config.prerenderUrls))) {
const prerenderUrls = tryResolveConfig(
cwd,
config.prerenderUrls,
config.prerenderUrls === 'prerender-urls.json',
config.verbose
);

if (prerenderUrls) {
try {
let result = require(resolve(cwd, config.prerenderUrls));
let result = esmImport(prerenderUrls);

if (typeof result.default !== 'undefined') {
result = result.default();
result = result.default;
}
if (typeof result === 'function') {
info(`Fetching URLs from ${config.prerenderUrls}`);
result = await result();
info(`Fetched URLs from ${config.prerenderUrls}`);
Comment on lines -117 to -119
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think these info messages really provided any useful information.

}
if (typeof result === 'string') {
result = JSON.parse(result);
}
if (result instanceof Array) {
if (Array.isArray(result)) {
Comment on lines -124 to +129
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure why this stopped working, but probably shouldn't be using instanceof anyhow

pages = result;
}
} catch (error) {
Expand All @@ -131,17 +136,6 @@ module.exports = async function (config) {
}`
);
}
// don't warn if the default file doesn't exist
} else if (
config.prerenderUrls !== 'prerender-urls.json' ||
config.verbose
) {
warn(
`prerenderUrls file (${resolve(
cwd,
config.prerenderUrls
)}) doesn't exist, using default!`
);
}
}
/**
Expand Down
36 changes: 19 additions & 17 deletions packages/cli/lib/lib/webpack/transform-config.js
@@ -1,7 +1,7 @@
const { resolve } = require('path');
const webpack = require('webpack');
const { stat } = require('fs').promises;
const { error } = require('../../util');
const { error, esmImport, tryResolveConfig, warn } = require('../../util');

const FILE = 'preact.config';
const EXTENSIONS = ['js', 'json'];
Expand Down Expand Up @@ -95,24 +95,26 @@ module.exports = async function (env, webpackConfig, isServer = false) {
env.config !== 'preact.config.js'
? { configFile: env.config, isDefault: false }
: await findConfig(env);
env.config = configFile;
let myConfig = resolve(env.cwd, env.config);

try {
await stat(myConfig);
} catch (e) {
if (isDefault) return;
throw new Error(
`preact-cli config could not be loaded!\nFile ${env.config} not found.`
);
}
const cliConfig = tryResolveConfig(
env.cwd,
configFile,
isDefault,
env.verbose
);

let m = require('esm')(module)(myConfig);
if (!cliConfig) return;

// The line above results in an empty object w/ Jest,
// so we need to do the following in order to load it:
if (Object.keys(m).length === 0) {
m = require(myConfig);
let m;
try {
m = esmImport(cliConfig);
} catch (error) {
warn(
`Failed to load preact-cli config file, using default!\n${
env.verbose ? error.stack : error.message
}`
);
return;
}

const transformers = parseConfig((m && m.default) || m);
Expand All @@ -131,7 +133,7 @@ module.exports = async function (env, webpackConfig, isServer = false) {
options
);
} catch (err) {
throw new Error((`Error at ${myConfig}: \n` + err && err.stack) || err);
throw new Error((`Error at ${cliConfig}: \n` + err && err.stack) || err);
}
}
};
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/lib/util.js
Expand Up @@ -30,10 +30,10 @@ exports.info = function (text, code) {
code && process.exit(code);
};

exports.warn = function (text, code) {
const warn = (exports.warn = function (text, code) {
process.stdout.write(symbols.warning + yellow(' WARN ') + text + '\n');
code && process.exit(code);
};
});

exports.error = function (text, code = 1) {
process.stderr.write(symbols.error + red(' ERROR ') + text + '\n');
Expand All @@ -56,6 +56,8 @@ exports.toBool = function (val) {
return val === void 0 || (val === 'false' ? false : val);
};

exports.esmImport = require('esm')(module);

/**
* Taken from: https://github.com/preactjs/wmr/blob/3401a9bfa6491d25108ad68688c067a7e17d0de5/packages/wmr/src/lib/net-utils.js#L4-Ll4
* Check if a port is free
Expand All @@ -78,3 +80,12 @@ exports.isPortFree = async function (port) {
return false;
}
};

exports.tryResolveConfig = function (cwd, file, isDefault, verbose) {
const path = resolve(cwd, file);
if (existsSync(path)) {
return path;
} else if (!isDefault || verbose) {
warn(`${resolve(cwd, file)} doesn't exist, using default!`);
}
};
4 changes: 2 additions & 2 deletions packages/cli/package.json
Expand Up @@ -34,9 +34,9 @@
"homepage": "https://github.com/preactjs/preact-cli",
"devDependencies": {
"@types/express": "^4.17.13",
"@types/jest": "^27.4.0",
"@types/jest": "^24.9.1",
"html-looks-like": "^1.0.2",
"jest": "^27.0.1",
"jest": "^24.9.0",
Copy link
Member Author

Choose a reason for hiding this comment

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

Chased down the weird config file hack I introduced in #1623, turns out to be an issue in esm (the package, not the format) in Jest > v25.

Issue filed at jestjs/jest#12493, but I needed to use ESM within Jest and that hack only worked for CJS config files in our test suite. Downgrading shouldn't be problematic, so I went this route.

"less": "^4.1.1",
"less-loader": "^7.3.0",
"ncp": "^2.0.0",
Expand Down
63 changes: 63 additions & 0 deletions packages/cli/tests/config-formats.test.js
@@ -0,0 +1,63 @@
const { join } = require('path');
const { access } = require('fs').promises;
const { build } = require('./lib/cli');
const { subject } = require('./lib/output');

const formats = ['cjs', 'esm'];

const prerenderUrlFiles = [
'array.js',
'stringified-array.js',
'function-returning-array.js',
'function-returning-stringified-array.js',
];

const preactConfigFiles = ['function.js', 'object.js'];

describe('config files', () => {
describe('prerender-urls', () => {
it(`should load the 'prerender-urls.json' file`, async () => {
let dir = await subject('multiple-config-files');

await build(dir);

expect(await access(join(dir, 'build/index.html'))).toBeUndefined();
expect(
await access(join(dir, 'build/custom/index.html'))
).toBeUndefined();
});

formats.forEach(moduleFormat => {
prerenderUrlFiles.forEach(dataFormat => {
it(`should load the '${dataFormat}' file in ${moduleFormat}`, async () => {
let dir = await subject('multiple-config-files');

await build(dir, {
prerenderUrls: `prerenderUrls/${moduleFormat}/${dataFormat}`,
});

expect(await access(join(dir, 'build/index.html'))).toBeUndefined();
expect(
await access(join(dir, 'build/custom/index.html'))
).toBeUndefined();
});
});
});
});

describe('preact.config', () => {
formats.forEach(moduleFormat => {
preactConfigFiles.forEach(dataFormat => {
it(`should load the '${dataFormat}' file in ${moduleFormat}`, async () => {
let dir = await subject('multiple-config-files');

await build(dir, {
config: `preactConfig/${moduleFormat}/${dataFormat}`,
});

expect(await access(join(dir, 'build/bundle.js'))).toBeUndefined();
});
});
});
});
});
19 changes: 19 additions & 0 deletions packages/cli/tests/subjects/multiple-config-files/index.js
@@ -0,0 +1,19 @@
import { h, Component } from 'preact';
import { Router } from 'preact-router';
import Home from './routes/home';

export default class App extends Component {
handleRoute = e => {
this.currentUrl = e.url;
};

render(props) {
return (
<div id="app">
<Router url={props.url} onChange={this.handleRoute} {...props}>
<Home path="/" />
</Router>
</div>
);
}
}
@@ -0,0 +1,4 @@
{
"private": true,
"name": "preact-config"
}
@@ -0,0 +1,3 @@
module.exports = function (config) {
config.output.filename = '[name].js';
};
@@ -0,0 +1,5 @@
module.exports = {
webpack(config) {
config.output.filename = '[name].js';
},
};
@@ -0,0 +1,3 @@
export default function (config) {
config.output.filename = '[name].js';
}
@@ -0,0 +1,5 @@
export default {
webpack(config) {
config.output.filename = '[name].js';
},
};
@@ -0,0 +1,8 @@
[
{
"url": "/"
},
{
"url": "/custom"
}
]
@@ -0,0 +1,8 @@
module.exports = [
{
url: '/',
},
{
url: '/custom',
},
];
@@ -0,0 +1,8 @@
module.exports = () => [
{
url: '/',
},
{
url: '/custom',
},
];
@@ -0,0 +1,8 @@
module.exports = () => `[
{
"url": "/"
},
{
"url": "/custom"
}
]`;
@@ -0,0 +1,8 @@
module.exports = `[
{
"url": "/"
},
{
"url": "/custom"
}
]`;
@@ -0,0 +1,8 @@
export default [
{
url: '/',
},
{
url: '/custom',
},
];
@@ -0,0 +1,8 @@
export default () => [
{
url: '/',
},
{
url: '/custom',
},
];
@@ -0,0 +1,8 @@
export default () => `[
{
"url": "/"
},
{
"url": "/custom"
}
]`;