initial commit

This commit is contained in:
Kegan Myers 2019-02-18 01:55:52 -06:00
commit 6c2183f19c
16 changed files with 556 additions and 0 deletions

6
.editorconfig Normal file
View file

@ -0,0 +1,6 @@
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

3
.npmignore Normal file
View file

@ -0,0 +1,3 @@
.npmignore
.editorconfig
.gitignore

13
.prettierrc Normal file
View file

@ -0,0 +1,13 @@
{
"printWidth": 80,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"semi": true,
"requirePragma": false,
"proseWrap": "preserve",
"arrowParens": "always",
"htmlWhitespaceSensitivity": "css"
}

7
LICENSE.md Normal file
View file

@ -0,0 +1,7 @@
Copyright 2019 Kegan Myers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

45
README.md Normal file
View file

@ -0,0 +1,45 @@
# @terribleplan/next-cloudflare
This is pretty rough right now, but it works and can hopefully used to foster discussion in the next.js project around a different/better/additional serverless interface.
## Quickstart
1. You must be using [my fork of next.js](https://github.com/terribleplan/next.js).
1. `git clone https://github.com/terribleplan/next.js`
1. `yarn && pushd packages/next-server && npm link && popd && pushd packages/next && npm link && npm link next-server && popd`
1. Install this package to your project
1. `yarn add @terribleplan/next-cloudflare`
1. Build things
1. `npx next build && npx next-cloudflare`
## Usage
This package is usable as an npm module as well as a CLI.
### CLI
#### --input
The project directory that houses the .next build output directory. Defaults to whatever directory the command is invoked from.
This is passed as `cwd` to the programmatic API
#### --output
Where to write the output to. Defaults to `cloudflare-bundle.js` in the input directory (which defaults to the current working directory).
### API
#### Usage
```
const nextCloudflare = require('@terribleplan/next-cloudflare');
const bundleString = await nextCloudflare(options);
console.log(bundleString);
```
#### options.cwd
The project directory that houses the .next build output directory. Defaults to `process.cwd()`.

47
bin/next-cloudflare.js Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env node
const argv = require('minimist')(process.argv.slice(2));
const fs = require('bluebird').promisifyAll(require('fs'));
const path = require('path');
const nextCloudflare = require('../index.js');
const run = async () => {
let { input } = argv;
if (!input) {
input = path.resolve(process.cwd());
console.log(`WARN: \`--input\` not specified, using \`${input}\``);
}
try {
await fs.accessAsync(input);
} catch (e) {
console.log(`FATAL: Unable to access directory ${input}`);
}
let { output } = argv;
if (!output) {
output = path.resolve(input, 'cloudflare-bundle.js');
console.log(`WARN: \`--output\` not specified, using \`${output}\``);
}
const contents = await nextCloudflare({
cwd: input,
});
if (Buffer.byteLength(contents) > 1048576) {
console.log(`WARN: Bundle size exceeds 1MB, it may not be accepted`);
}
await fs.writeFileAsync(output, contents);
};
run().then(
() => {
console.log('INFO: Built cloudflare bundle ok');
process.exit(0);
},
(e) => {
console.log('INFO: Failed to build cloudflare bundle');
console.log(e);
process.exit(1);
},
);

24
index.js Normal file
View file

@ -0,0 +1,24 @@
const path = require('path');
const compose = require('./lib/asyncCompose');
const generatePage = require('./stages/generatePage');
const buildPage = require('./stages/buildPage');
const compressPage = require('./stages/compressPage');
const processPath = (...parts) => ['', ...parts].join('/').replace(/\/+/g, '/');
const nextCloudflare = async ({ cwd = process.cwd() }) => {
const basePath = path.resolve(cwd, '.next');
const unifiedPath = path.join(basePath, 'unified/index.js');
// todo: handle static files here, pass them into the main pipeline
return await compose(
generatePage,
buildPage(cwd),
compressPage,
)(unifiedPath);
};
module.exports = nextCloudflare;

24
lib/adapter.js Normal file
View file

@ -0,0 +1,24 @@
const url = require('url');
module.exports = (page) => {
const handler = async (request) => {
const { pathname, query } = url.parse(request.url, true);
// todo: figure out static asset serving
if (pathname.startsWith('/_next/')) {
return new Response('', {
status: 404,
});
}
const { status, headers, body } = await page.render(pathname, query);
// todo: consider etag handling
return new Response(body, {
status,
headers: new Headers(headers),
});
};
return handler;
};

13
lib/asyncCompose.js Normal file
View file

@ -0,0 +1,13 @@
const asyncCompose = (...funcs) => {
return async (argument) => {
let current = argument;
for (const func of funcs) {
current = await func(current);
}
return current;
};
};
module.exports = asyncCompose;

4
lib/polyfill-path.js Normal file
View file

@ -0,0 +1,4 @@
// todo: figure out at what point this isn't necessary.
// path-browserify supposedly added support for .posix at some point
const path = require('path-browserify');
module.exports.posix = require('path-browserify');

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "@terribleplan/next-cloudflare",
"version": "0.0.1",
"description": "A way to build next.js apps for cloudflare workers",
"repository": "https://github.com/terribleplan/next-cloudflare",
"author": "Kegan Myers <kegan@keganmyers.com>",
"license": "MIT",
"main": "index.js",
"bin": {
"next-cloudflare": "bin/next-cloudflare.js"
},
"dependencies": {
"bluebird": "^3.5.3",
"glob": "^7.1.3",
"glob-promise": "^3.4.0",
"memory-fs": "^0.4.1",
"minimist": "^1.2.0",
"terser": "^3.16.1"
},
"peerDependencies": {
"next": "^8.0.0",
"webpack": "^4.29.0",
"node-libs-browser": "^2.0.0"
}
}

102
stages/buildPage.js Normal file
View file

@ -0,0 +1,102 @@
const MemoryFS = require('memory-fs');
const path = require('path');
const realFs = require('fs');
const pfs = require('bluebird').promisifyAll(require('fs'));
const webpack = require('webpack');
const { NormalModuleReplacementPlugin } = webpack;
const buildPage = (projectDir) => async (page) => {
const fs = new MemoryFS();
const memStat = fs.stat.bind(fs);
fs.stat = function(path, ...args) {
if (path === '/entry.js') {
return memStat(path, ...args);
}
return realFs.stat(path, ...args);
};
const memReadFile = fs.readFile.bind(fs);
fs.readFile = function(path, ...args) {
if (path === '/entry.js') {
return memReadFile(path, ...args);
}
return realFs.readFile(path, ...args);
};
let writtenPath = null;
const memWriteFile = fs.writeFile.bind(fs);
fs.writeFile = function(path, ...args) {
writtenPath = path;
return memWriteFile(path, ...args);
};
fs.writeFileSync('/entry.js', page);
const webpackConfig = {
mode: 'production',
target: 'webworker',
plugins: [
new NormalModuleReplacementPlugin(
/^path$/,
path.resolve(__dirname, '../lib/polyfill-path.js'),
),
],
node: {
console: false,
global: true,
process: true,
__filename: 'mock',
__dirname: 'mock',
Buffer: true,
setImmediate: true,
dns: false,
fs: 'empty',
path: false,
crypto: 'empty',
url: true,
zlib: false,
},
entry: '/entry.js',
output: { filename: 'output.js' },
resolve: { modules: [] },
};
webpackConfig.resolve.modules = Array.from(
new Set(
(await Promise.all(
[
// hopefully the n_m for the project
path.resolve(__dirname, '../'),
// also hopefully the n_m for the project
path.resolve(projectDir, 'node_modules'),
// local n_m
path.resolve(__dirname, '../node_modules'),
// last resort fallback n_m for the project
path.resolve(process.cwd(), 'node_modules'),
].map(async (path) => {
try {
await pfs.accessAsync(path);
return path;
} catch (e) {}
return false;
}),
)).filter((x) => !!x),
),
);
const compiler = webpack(webpackConfig);
compiler.outputFileSystem = compiler.inputFileSystem = fs;
return await new Promise((res, rej) => {
compiler.run((err, stats) => {
if (err) {
return rej(err);
}
if (!writtenPath) {
console.log(stats.compilation.errors);
return rej(new Error('failed to compile'));
}
res(fs.readFileSync(writtenPath).toString());
});
});
};
module.exports = buildPage;

13
stages/compressPage.js Normal file
View file

@ -0,0 +1,13 @@
const terser = require('terser');
const compressPage = (page) =>
terser.minify(page, {
toplevel: true,
compress: {
passes: 3,
unsafe: false,
pure_getters: true,
},
}).code;
module.exports = compressPage;

16
stages/generatePage.js Normal file
View file

@ -0,0 +1,16 @@
const path = require('path');
const makePageString = (pagePath) => `
const page = require(${JSON.stringify(pagePath)});
const adapter = require(${JSON.stringify(
path.resolve(__dirname, '../lib/adapter'),
)})
const handler = adapter(page);
addEventListener('fetch', (event) => {
event.respondWith(handler(event.request));
});
`;
module.exports = makePageString;

213
yarn.lock Normal file
View file

@ -0,0 +1,213 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/glob@*":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*":
version "11.9.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.4.tgz#ceb0048a546db453f6248f2d1d95e937a6f00a14"
integrity sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
bluebird@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
commander@~2.17.1:
version "2.17.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
errno@^0.1.3:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
dependencies:
prr "~1.0.1"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob-promise@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.4.0.tgz#b6b8f084504216f702dc2ce8c9bc9ac8866fdb20"
integrity sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==
dependencies:
"@types/glob" "*"
glob@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
memory-fs@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
dependencies:
errno "^0.1.3"
readable-stream "^2.0.1"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
readable-stream@^2.0.1:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
source-map-support@~0.5.9:
version "0.5.10"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c"
integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
terser@^3.16.1:
version "3.16.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493"
integrity sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==
dependencies:
commander "~2.17.1"
source-map "~0.6.1"
source-map-support "~0.5.9"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=