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

Generate manifest from original. Better export. #59

Merged
merged 3 commits into from
Sep 21, 2018
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
5 changes: 5 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 100,
"trailingComma": "all",
"singleQuote": true
Copy link
Contributor Author

Choose a reason for hiding this comment

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

put some defaults that I use personally. the only difference I think was the trailingComma option. what do you prefer?

Copy link
Owner

Choose a reason for hiding this comment

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

Trailing makes sense

}
80 changes: 20 additions & 60 deletions export.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,24 @@
const { readFile, writeFile } = require('fs-extra');
const Precache = require('./next-files');
const { generateSW, injectManifest } = require('workbox-build');
const { join, resolve } = require('path');
const parseArgs = require('minimist');

const dev = process.env.NODE_ENV !== 'production';

module.exports = Export;

async function Export(nextConfig) {
const {
distDir = '.next',
exportPathMap,
generateSw = true,
workboxOpts = {
runtimeCaching: [{ urlPattern: /^https?.*/, handler: 'networkFirst' }]
const { copy } = require('fs-extra');
const { join } = require('path');

/**
* Copy the generated service worker into the export folder.
*/
function exportSw(nextConfig) {
return async function exportPathMap(...args) {
const [defaultPathMap, { dev, distDir, outDir }] = args;
const swDest = nextConfig.workboxOpts.swDest || 'service-worker.js';

if (dev) {
return defaultPathMap;
}
} = nextConfig;

if (dev || typeof exportPathMap !== 'function') {
return {};
}

// Logic for working out dir and outdir copied from `next-export`:
// https://github.com/zeit/next.js/blob/15dde33794622919d20709da97fa412a01831807/bin/next-export
const argv = parseArgs(process.argv.slice(2), {
alias: { o: 'outdir' },
default: { o: null }
});
const dir = argv._[0] || '.';
const outDir = argv.outdir ? resolve(argv.outdir) : resolve(dir, 'out');

const nextDir = join(process.cwd(), dir, distDir);
const buildIdPath = join(nextDir, 'BUILD_ID');
const buildId = await readFile(buildIdPath, 'utf8');

const { precaches } = await Precache({ buildId, nextDir });
// Copy service worker from Next.js build dir into the export dir.
await copy(join(distDir, swDest), join(outDir, swDest));

const swDest = join(outDir, 'service-worker.js');

if (generateSw) {
// globDirectory is intentionally left blank as it's required by workbox
await generateSW({ swDest, globDirectory: ' ', ...workboxOpts });
} else {
// So that the same file works as part of `next build` and `next export`
const injectionPointRegexp = /(__precacheManifest\s*=\s*)\[\](.*)/;
await injectManifest({
swDest,
globDirectory: ' ',
injectionPointRegexp,
...workboxOpts
});
}

const serviceWorkerContent = await readFile(swDest, 'utf8');
const newServiceWorkerContent = `self.__precacheManifest = ${JSON.stringify(
precaches
)};\n${serviceWorkerContent}`;

writeFile(swDest, newServiceWorkerContent);

return nextConfig.exportPathMap();
// Run user's exportPathMap function if available.
return nextConfig.exportPathMap ? nextConfig.exportPathMap(...args) : defaultPathMap;
};
}

module.exports = exportSw;
29 changes: 16 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
const { GenerateSW, InjectManifest } = require('workbox-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { join } = require('path');

const SwGen = require('./plugin');
const Export = require('./export');
const InlineNextPrecacheManifestPlugin = require('./plugin');
const exportSw = require('./export');

module.exports = (nextConfig = {}) => ({
...nextConfig,
async exportPathMap() {
return await Export(nextConfig);
},
exportPathMap: exportSw(nextConfig),
webpack(config, options) {
if (!options.defaultLoaders) {
throw new Error(
'This plugin is not compatible with Next.js versions below 5.0.0 https://err.sh/next-plugins/upgrade'
'This plugin is not compatible with Next.js versions below 5.0.0 https://err.sh/next-plugins/upgrade',
);
}

Expand All @@ -25,8 +24,8 @@ module.exports = (nextConfig = {}) => ({
workboxOpts = {
globPatterns: ['static/**/*'],
globDirectory: '.',
runtimeCaching: [{ urlPattern: /^https?.*/, handler: 'networkFirst' }]
}
runtimeCaching: [{ urlPattern: /^https?.*/, handler: 'networkFirst' }],
},
} = nextConfig;

// Generate SW
Expand All @@ -36,10 +35,14 @@ module.exports = (nextConfig = {}) => ({
} else if (!options.isServer) {
// Only run once for the client build.
config.plugins.push(
generateSw
? new GenerateSW({ ...workboxOpts })
: new InjectManifest({ ...workboxOpts }),
new SwGen({ buildId: options.buildId, assetPrefix })
new CleanWebpackPlugin(['precache-manifest.*.js'], { root: config.output.path }),
generateSw ? new GenerateSW({ ...workboxOpts }) : new InjectManifest({ ...workboxOpts }),
new InlineNextPrecacheManifestPlugin({
outputPath: config.output.path,
urlPrefix: assetPrefix,
manifestDest: 'precache-manifest.*.js',
swDest: workboxOpts.swDest || 'service-worker.js',
}),
);
}

Expand All @@ -58,5 +61,5 @@ module.exports = (nextConfig = {}) => ({
}

return config;
}
},
});
42 changes: 0 additions & 42 deletions next-files.js

This file was deleted.

64 changes: 64 additions & 0 deletions next-manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const glob = require('glob');
const { readFile, writeFile } = require('fs-extra');
const { join } = require('path');

const nextUrlPrefix = '/_next/';
const excludeFiles = ['react-loadable-manifest.json', 'build-manifest.json'];

/**
* Workbox already generates a pretty good precache manifest for all the emitted
* assets. There are a few things different though:
* - there are emitted files that are not used and don't need to be cached
* - all emitted assets need to be prefixed with /_next/
* - we don't need the revision as all the precacheable files are versioned
*
* At the end replace old manifest reference with new inlined one.
*/
async function generateNextManifest(options) {
const manifestFilePath = join(options.outputPath, options.manifestDest);
const swFilePath = join(options.outputPath, options.swDest);

const originalManifest = await getOriginalManifest(manifestFilePath);
const nextManifest = buildNextManifest(originalManifest, options.assetPrefix);
await inlineManifest(nextManifest, swFilePath, options.manifestDest);
}

function getOriginalManifest(manifestFilePath) {
return new Promise((resolve, reject) => {
glob(manifestFilePath, async (err, files = []) => {
if (err) {
return reject(err);
}

// Pick first and only as we've clean old ones.
const file = await readFile(files[0], 'utf-8');
// Execute file with a self variable in the scope/context.
const self = {};
new Function('self', file)(self);

resolve(self.__precacheManifest);
});
});
}

function buildNextManifest(originalManifest, urlPrefix = '') {
return originalManifest.filter(entry => !excludeFiles.includes(entry.url)).map(entry => ({
url: `${urlPrefix}${nextUrlPrefix}${entry.url}`,
}));
}

async function inlineManifest(manifest, swFilePath, precachePath) {
const originalSw = await readFile(swFilePath, 'utf8');

// Prepend/inline newly generated precache manifest and remove import for old one.
const manifestImportRegex = new RegExp(`(,\s*)?"${precachePath}"`);
const newSw = `self.__precacheManifest = ${JSON.stringify(
manifest,
null,
2,
)};\n\n${originalSw.replace(manifestImportRegex, '')}`;

await writeFile(swFilePath, newSw, 'utf8');
}

module.exports = generateNextManifest;
20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"authors": [
"Jack Hanford"
],
"contributors": [
"Joao Vieira <joaoguerravieira@gmail.com>"
],
"tags": [
"react",
"next.js",
Expand All @@ -36,21 +39,20 @@
"sw-helpers"
],
"peerDependencies": {
"next": "^5.0.0 || ^6.0.0 || ^7.0.0"
"next": ">=7.0.0"
},
"dependencies": {
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "~4.5.2",
"fs-extra": "~5.0.0",
"glob": "~7.0.0",
"minimist": "1.2.0",
"webpack": "~3.11.0",
"workbox-build": "^3.5.0",
"workbox-webpack-plugin": "^3.5.0"
"fs-extra": "~7.0.0",
"glob": "~7.1.3",
"webpack": "^4.19.1",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

still required by workbox plugin even though it's not a peer dependency 😩

"workbox-webpack-plugin": "^3.6.1"
},
"devDependencies": {
"husky": "^0.14.3",
"prettier": "^1.14.2",
"pretty-quick": "^1.6.0"
"prettier": "^1.14.3",
"pretty-quick": "^1.7.0"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove unused ones and updated all packages

},
"husky": {
"hooks": {
Expand Down
43 changes: 5 additions & 38 deletions plugin.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,19 @@
const fs = require('fs-extra');
const { join } = require('path');
const nextFiles = require('./next-files.js');
const generateNextManifest = require('./next-manifest');

module.exports = class NextFilePrecacherPlugin {
module.exports = class InlineNextPrecacheManifestPlugin {
constructor(opts) {
this.opts = {
filename: 'service-worker.js',
...opts
};
this.opts = opts;
}

apply(compiler) {
compiler.plugin('after-emit', (compilation, callback) => {
this.opts.filename = join(
compiler.options.output.path,
this.opts.filename
);
this.opts.outputPath = compiler.options.output.path;

callback();
});

compiler.plugin(
'done',
async () => {
const sw = await fs.readFile(
join(this.opts.outputPath, 'service-worker.js'),
'utf8'
);
const newPrecacheManifest = await nextFiles({
buildId: this.opts.buildId,
nextDir: this.opts.outputPath,
assetPrefix: this.opts.assetPrefix
});

// Prepend/inline newly generated precache manifest and remove import for old one.
const manifestImportRegex = /(,\s*)?"precache-manifest\..*\.js"/;
const newSw = `self.__precacheManifest = ${JSON.stringify(
newPrecacheManifest,
null,
2
)};\n\n${sw.replace(manifestImportRegex, '')}`;

return await fs.writeFile(this.opts.filename, newSw, 'utf8');
await generateNextManifest(this.opts);
},
err => {
throw new Error(`Precached failed: ${err.toString()}`);
}
},
);
}
};
Loading