diff --git a/.github/workflows/ci-exp.yml b/.github/workflows/ci-exp.yml
new file mode 100644
index 000000000..24b429750
--- /dev/null
+++ b/.github/workflows/ci-exp.yml
@@ -0,0 +1,28 @@
+name: Continuous Integration (Experimental)
+
+on: [pull_request]
+
+jobs:
+
+ build:
+ runs-on: ubuntu-18.04
+
+ strategy:
+ matrix:
+ node: [16]
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Install Chromium Library Dependencies
+ run: |
+ sh ./.github/workflows/chromium-lib-install.sh
+ - name: Use Node.js ${{ matrix.node }}
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node }}
+ - name: Installing project dependencies
+ run: |
+ yarn install --frozen-lockfile && yarn lerna bootstrap
+ - name: Test
+ run: |
+ yarn test:exp
\ No newline at end of file
diff --git a/.github/workflows/ci-win-exp.yml b/.github/workflows/ci-win-exp.yml
new file mode 100644
index 000000000..2d44f8e86
--- /dev/null
+++ b/.github/workflows/ci-win-exp.yml
@@ -0,0 +1,25 @@
+name: Continuous Integration Windows (Experimental)
+
+on: [pull_request]
+
+jobs:
+
+ build:
+ runs-on: windows-latest
+
+ strategy:
+ matrix:
+ node: [16]
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Use Node.js ${{ matrix.node }}
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node }}
+ - name: Installing project dependencies
+ run: |
+ yarn install --frozen-lockfile --network-timeout 1000000 && yarn lerna bootstrap
+ - name: Test
+ run: |
+ yarn test:exp:win
\ No newline at end of file
diff --git a/.mocharc.cjs b/.mocharc.cjs
index 5eb4c48b5..51980dafe 100644
--- a/.mocharc.cjs
+++ b/.mocharc.cjs
@@ -1,6 +1,3 @@
-const path = require('path');
-
module.exports = {
- spec: path.join(__dirname, 'packages/**/test/**/**/**/*.spec.js'),
- timeout: 60000
+ timeout: 90000
};
\ No newline at end of file
diff --git a/.nvmrc b/.nvmrc
index 62df50f1e..975215f45 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-14.17.0
+16.17.0
\ No newline at end of file
diff --git a/greenwood.config.js b/greenwood.config.js
index e913c0891..332980001 100644
--- a/greenwood.config.js
+++ b/greenwood.config.js
@@ -15,11 +15,11 @@ export default {
staticRouter: true,
interpolateFrontmatter: true,
plugins: [
- ...greenwoodPluginGraphQL(),
- ...greenwoodPluginPolyfills(),
+ greenwoodPluginGraphQL(),
+ greenwoodPluginPolyfills(),
greenwoodPluginPostCss(),
- ...greenwoodPluginImportJson(),
- ...greenwoodPluginImportCss(),
+ greenwoodPluginImportJson(),
+ greenwoodPluginImportCss(),
{
type: 'rollup',
name: 'rollup-plugin-analyzer',
@@ -34,8 +34,8 @@ export default {
];
}
},
- ...greenwoodPluginIncludeHTML(),
- ...greenwoodPluginRendererPuppeteer()
+ greenwoodPluginIncludeHTML(),
+ greenwoodPluginRendererPuppeteer()
],
markdown: {
plugins: [
diff --git a/lerna.json b/lerna.json
index a7bf1b08a..ed550a6fa 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "0.26.2",
+ "version": "0.27.0-alpha.7",
"packages": [
"packages/*",
"www"
diff --git a/netlify.toml b/netlify.toml
index 2afe51a55..5c42cda2e 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -6,4 +6,8 @@
skip_processing = true
[build.environment]
- NODE_VERSION = "14.16.0"
\ No newline at end of file
+ NODE_VERSION = "14.16.0"
+
+[[redirects]]
+ from = "/docs/tech-stack/"
+ to = "/about/tech-stack/"
\ No newline at end of file
diff --git a/package.json b/package.json
index ef1270d69..499ebbf4a 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,9 @@
"build": "cross-env __GWD_ROLLUP_MODE__=strict node . build",
"serve": "node . serve",
"develop": "node . develop",
- "test": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict c8 mocha",
+ "test": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict c8 mocha --exclude \"./packages/**/test/cases/exp-*/**\" \"./packages/**/**/*.spec.js\"",
+ "test:exp": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --experimental-loader $(pwd)/test/test-loader.js ./node_modules/mocha/bin/mocha \"./packages/**/**/*.spec.js\"",
+ "test:exp:win": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --experimental-loader file:\\\\%cd%\\test\\test-loader.js ./node_modules/mocha/bin/mocha --exclude \"./packages/init/test/cases/**\" \"./packages/**/**/*.spec.js\"",
"test:tdd": "yarn test --watch",
"lint:js": "eslint \"*.js\" \"./packages/**/**/*.js\" \"./test/*.js\" \"./www/**/**/*.js\"",
"lint:ts": "eslint \"./packages/**/**/*.ts\"",
@@ -40,7 +42,7 @@
"cross-env": "^7.0.3",
"eslint": "^6.8.0",
"eslint-plugin-no-only-tests": "^2.6.0",
- "gallinago": "^0.5.0",
+ "gallinago": "^0.6.0",
"glob-promise": "^3.4.0",
"jsdom": "^16.5.0",
"lerna": "^3.16.4",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 6f744a74a..2f7d23e24 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@greenwood/cli",
- "version": "0.26.2",
+ "version": "0.27.0-alpha.7",
"description": "Greenwood CLI.",
"type": "module",
"repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli",
@@ -29,7 +29,7 @@
"acorn": "^8.0.1",
"acorn-walk": "^8.0.0",
"commander": "^2.20.0",
- "cssnano": "^5.0.11",
+ "css-tree": "^2.2.1",
"es-module-shims": "^1.2.0",
"front-matter": "^4.0.2",
"koa": "^2.13.0",
@@ -37,8 +37,6 @@
"markdown-toc": "^1.2.0",
"node-fetch": "^2.6.1",
"node-html-parser": "^1.2.21",
- "postcss": "^8.3.11",
- "postcss-import": "^13.0.0",
"rehype-raw": "^5.0.0",
"rehype-stringify": "^8.0.0",
"remark-frontmatter": "^2.0.0",
@@ -47,7 +45,7 @@
"rollup": "^2.58.0",
"rollup-plugin-terser": "^7.0.0",
"unified": "^9.2.0",
- "wc-compiler": "~0.5.0"
+ "wc-compiler": "~0.6.1"
},
"devDependencies": {
"@babel/runtime": "^7.10.4",
diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js
index 27f78eaf5..e5ce43348 100644
--- a/packages/cli/src/commands/build.js
+++ b/packages/cli/src/commands/build.js
@@ -11,10 +11,7 @@ const runProductionBuild = async (compilation) => {
try {
const { prerender } = compilation.config;
const outputDir = compilation.context.outputDir;
- const defaultPrerender = (compilation.config.plugins.filter(plugin => plugin.type === 'renderer' && plugin.isGreenwoodDefaultPlugin) || []).length === 1
- ? compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation)
- : {};
- const customPrerender = (compilation.config.plugins.filter(plugin => plugin.type === 'renderer' && !plugin.isGreenwoodDefaultPlugin) || []).length === 1
+ const prerenderPlugin = (compilation.config.plugins.filter(plugin => plugin.type === 'renderer') || []).length === 1
? compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation)
: {};
@@ -22,7 +19,7 @@ const runProductionBuild = async (compilation) => {
fs.mkdirSync(outputDir);
}
- if (prerender || customPrerender.prerender) {
+ if (prerender || prerenderPlugin.prerender) {
// start any servers if needed
const servers = [...compilation.config.plugins.filter((plugin) => {
return plugin.type === 'server';
@@ -42,14 +39,10 @@ const runProductionBuild = async (compilation) => {
return Promise.resolve(server);
}));
- if (customPrerender.workerUrl) {
- await preRenderCompilationWorker(compilation, customPrerender);
- } else if (customPrerender.customUrl) {
- await preRenderCompilationCustom(compilation, customPrerender);
- } else if (defaultPrerender && prerender) {
- await preRenderCompilationWorker(compilation, defaultPrerender);
+ if (prerenderPlugin.workerUrl) {
+ await preRenderCompilationWorker(compilation, prerenderPlugin);
} else {
- reject('This is an unhandled pre-rendering case! Please report.');
+ await preRenderCompilationCustom(compilation, prerenderPlugin);
}
} else {
await staticRenderCompilation(compilation);
diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js
index 8176220e9..e488b1e45 100644
--- a/packages/cli/src/config/rollup.config.js
+++ b/packages/cli/src/config/rollup.config.js
@@ -1,555 +1,141 @@
-/* eslint-disable max-depth, no-loop-func */
import fs from 'fs';
-import htmlparser from 'node-html-parser';
import path from 'path';
-import postcss from 'postcss';
-import postcssImport from 'postcss-import';
+import { terser } from 'rollup-plugin-terser';
-const tokenSuffix = 'scratch';
-const tokenNodeModules = 'node_modules';
-
-const hashString = (queryKeysString) => {
- let h = 0;
-
- for (let i = 0; i < queryKeysString.length; i += 1) {
- h = Math.imul(31, h) + queryKeysString.charCodeAt(i) | 0; // eslint-disable-line no-bitwise
- }
-
- return Math.abs(h).toString();
-};
-
-const parseTagForAttributes = (tag) => {
- return tag.rawAttrs.split(' ').map((attribute) => {
- if (attribute.indexOf('=') > 0) {
- const attributePieces = attribute.split('=');
- return {
- [attributePieces[0]]: attributePieces[1].replace(/"/g, '').replace(/'/g, '')
- };
- } else {
- return undefined;
- }
- }).filter(attribute => attribute)
- .reduce((accum, attribute) => {
- return Object.assign(accum, {
- ...attribute
- });
- }, {});
-};
-
-async function getOptimizedSource(url, plugins, compilation) {
- const initSoure = fs.readFileSync(url, 'utf-8');
- let optimizedSource = await plugins.reduce(async (bodyPromise, resource) => {
- const body = await bodyPromise;
- const shouldOptimize = await resource.shouldOptimize(url, body);
-
- if (shouldOptimize) {
- const optimizedBody = await resource.optimize(url, body);
-
- return Promise.resolve(optimizedBody);
- } else {
- return Promise.resolve(body);
- }
- }, Promise.resolve(initSoure));
-
- // if no custom user optimization found, fallback to standard Greenwood default optimization
- if (optimizedSource === initSoure) {
- const standardResourcePlugins = compilation.config.plugins
- .filter((plugin) => {
- return plugin.type === 'resource'
- && plugin.name.indexOf('plugin-standard') === 0;
- }).map((plugin) => {
- return plugin.provider(compilation);
- });
-
- optimizedSource = await standardResourcePlugins.reduce(async (sourcePromise, resource) => {
- const source = await sourcePromise;
- const shouldOptimize = await resource.shouldOptimize(url, source);
-
- if (shouldOptimize) {
- const defaultOptimizedSource = await resource.optimize(url, source);
-
- return Promise.resolve(defaultOptimizedSource);
- } else {
- return Promise.resolve(source);
- }
- }, Promise.resolve(optimizedSource));
- }
-
- return Promise.resolve(optimizedSource);
-}
-
-function greenwoodWorkspaceResolver (compilation) {
- const { userWorkspace, scratchDir } = compilation.context;
-
- return {
- name: 'greenwood-workspace-resolver',
- resolveId(source) {
- if ((source.indexOf('./') === 0 || source.indexOf('/') === 0) && path.extname(source) !== '.html' && fs.existsSync(path.join(userWorkspace, source))) {
- return source.replace(source, path.join(userWorkspace, source));
- }
-
- // handle inline script / style bundling
- if (source.indexOf(`-${tokenSuffix}`) > 0 && fs.existsSync(path.join(scratchDir, source))) {
- return source.replace(source, path.join(scratchDir, source));
- }
-
- return null;
- }
- };
-}
-
-// https://github.com/rollup/rollup/issues/2873
-function greenwoodHtmlPlugin(compilation) {
- const { projectDirectory, userWorkspace, outputDir, scratchDir } = compilation.context;
- const { optimization } = compilation.config;
- const isRemoteUrl = (url = undefined) => url && (url.indexOf('http') === 0 || url.indexOf('//') === 0);
- const customResources = compilation.config.plugins.filter((plugin) => {
- return plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin;
+function greenwoodResourceLoader (compilation) {
+ const resourcePlugins = compilation.config.plugins.filter((plugin) => {
+ return plugin.type === 'resource';
}).map((plugin) => {
return plugin.provider(compilation);
});
return {
- name: 'greenwood-html-plugin',
- // tell Rollup how to handle HTML entry points
- // and other custom user resource types like .ts, .gql, etc
- async load(id) {
- const extension = path.extname(id);
- const importAsRegex = /\?type=(.*)/;
+ name: 'greenwood-resource-loader',
+ resolveId(id) {
+ const { userWorkspace } = compilation.context;
- // bit of a hack to get these two bugs to play well together
- // https://github.com/ProjectEvergreen/greenwood/issues/598
- // https://github.com/ProjectEvergreen/greenwood/issues/604
- if (importAsRegex.test(id)) {
- const match = id.match(importAsRegex);
- const importee = id
- .replace(match[0], '')
- .replace(/\\/g, '/');
-
- return `export {default} from '${importee}';`;
+ if ((id.indexOf('./') === 0 || id.indexOf('/') === 0) && fs.existsSync(path.join(userWorkspace, id))) {
+ return path.join(userWorkspace, id.replace(/\?type=(.*)/, ''));
}
- switch (extension) {
-
- case '.html':
- return Promise.resolve('');
- default:
- const resourceHandler = (await Promise.all(customResources.map(async (resource) => {
- const shouldServe = await resource.shouldServe(id);
-
- return shouldServe
- ? resource
- : null;
- }))).filter(resource => resource);
-
- if (resourceHandler.length) {
- const response = await resourceHandler[0].serve(id);
-
- return Promise.resolve(response.body);
- }
- break;
-
- }
+ return null;
},
-
- // crawl through all entry HTML files and emit JavaScript chunks and CSS assets along the way
- // for bundling with Rollup
- buildStart(options) {
- const mappedStyles = [];
- const mappedScripts = new Map();
-
- for (const input in options.input) {
- try {
- const inputHtml = options.input[input];
- const html = fs.readFileSync(inputHtml, 'utf-8');
- const root = htmlparser.parse(html, {
- script: true,
- style: true
- });
- const headScripts = root.querySelectorAll('script');
- const headLinks = root.querySelectorAll('link');
-
- headScripts.forEach((scriptTag) => {
- const parsedAttributes = parseTagForAttributes(scriptTag);
- // handle
- if (!isRemoteUrl(parsedAttributes.src) && parsedAttributes.src && !mappedScripts.get(parsedAttributes.src)) {
- if (optimization === 'static' || parsedAttributes['data-gwd-opt'] === 'static') {
- // dont need to bundle / emit this one
- } else {
- const { src } = parsedAttributes;
- const absoluteSrc = `${path.normalize(src.replace(/\.\.\//g, '').replace('./', ''))}`;
- const basePath = absoluteSrc.indexOf(tokenNodeModules) >= 0
- ? projectDirectory
- : userWorkspace;
- const id = path.join(basePath, absoluteSrc);
- const source = fs.readFileSync(id, 'utf-8');
-
- mappedScripts.set(absoluteSrc, true);
-
- this.emitFile({
- type: 'chunk',
- id,
- name: absoluteSrc.split(`${path.sep}`)[absoluteSrc.split(`${path.sep}`).length - 1].replace('.js', ''),
- source
- });
- }
- }
-
- // handle
- if (parsedAttributes.type === 'module' && scriptTag.rawText !== '') {
- const id = hashString(scriptTag.rawText);
-
- if (!mappedScripts.get(id)) {
- // using console.log avoids having rollup strip out our internal marker if we used a commnent
- const marker = `${id}-${tokenSuffix}`;
- const filename = `${marker}.js`;
- const source = `${scriptTag.rawText}console.log("${marker}");`.trim();
-
- fs.writeFileSync(path.join(scratchDir, filename), source);
- mappedScripts.set(id, true);
-
- this.emitFile({
- type: 'chunk',
- id: filename,
- name: filename.replace('.js', ''),
- source
- });
- }
+ async load(id) {
+ const importAsIdAsUrl = id.replace(/\?type=(.*)/, '');
+ const extension = path.extname(importAsIdAsUrl);
+
+ if (extension !== '.js') {
+ const originalUrl = `${id}?type=${extension.replace('.', '')}`;
+ let contents;
+
+ for (const plugin of resourcePlugins) {
+ const headers = {
+ request: {
+ originalUrl
+ },
+ response: {
+ 'content-type': plugin.contentType
}
- });
-
- headLinks.forEach((linkTag) => {
- const parsedAttributes = parseTagForAttributes(linkTag);
-
- // handle
- if (!isRemoteUrl(parsedAttributes.href) && parsedAttributes.rel === 'stylesheet' && !mappedStyles[parsedAttributes.href]) {
- let { href } = parsedAttributes;
-
- if (href.charAt(0) === '/') {
- href = href.slice(1);
- }
-
- const basePath = href.indexOf(tokenNodeModules) >= 0
- ? projectDirectory
- : userWorkspace;
- const absoluteHref = href.replace(/\.\.\//g, '').replace('./', '');
- const filePath = path.join(basePath, absoluteHref);
- const source = fs.readFileSync(filePath, 'utf-8');
- const to = `${outputDir}/${absoluteHref}`;
- const hash = hashString(source);
- const fileName = absoluteHref.replace('.css', `.${hash.slice(0, 8)}.css`);
+ };
- if (!fs.existsSync(path.dirname(to)) && href.indexOf(tokenNodeModules) < 0) {
- fs.mkdirSync(path.dirname(to), {
- recursive: true
- });
- }
+ contents = await plugin.shouldServe(importAsIdAsUrl)
+ ? (await plugin.serve(importAsIdAsUrl)).body
+ : contents;
- mappedStyles[fileName] = {
- type: 'asset',
- fileName: fileName.indexOf(tokenNodeModules) >= 0
- ? path.basename(fileName)
- : fileName,
- name: href,
- source
- };
- }
- });
- } catch (e) {
- console.error(e);
+ if (await plugin.shouldIntercept(importAsIdAsUrl, contents, headers)) {
+ contents = (await plugin.intercept(importAsIdAsUrl, contents, headers)).body;
+ }
}
- }
-
- // this is a giant work around because PostCSS and some plugins can only be run async
- // and so have to use with await but _outside_ sync code, like parser / rollup
- // https://github.com/cssnano/cssnano/issues/68
- // https://github.com/postcss/postcss/issues/595
- Promise.all(Object.keys(mappedStyles).map(async (assetKey) => {
- const asset = mappedStyles[assetKey];
- const source = mappedStyles[assetKey].source;
- const basePath = asset.name.indexOf(tokenNodeModules) >= 0
- ? projectDirectory
- : userWorkspace;
- const result = await postcss()
- .use(postcssImport())
- .process(source, {
- from: path.join(basePath, asset.name.replace(/\.\.\//g, ''))
- });
- asset.source = result.css;
+ return contents;
+ }
+ }
+ };
+}
- return new Promise((resolve, reject) => {
- try {
- this.emitFile(asset);
- resolve();
- } catch (e) {
- reject(e);
+function greenwoodSyncPageResourceBundlesPlugin(compilation) {
+ return {
+ name: 'greenwood-sync-page-resource-bundles-plugin',
+ writeBundle(outputOptions, bundles) {
+ const { outputDir } = compilation.context;
+
+ for (const resource of compilation.resources.values()) {
+ const resourceKey = resource.sourcePathURL.pathname;
+
+ for (const bundle in bundles) {
+ let facadeModuleId = (bundles[bundle].facadeModuleId || '').replace(/\\/g, '/');
+
+ /*
+ * this is an odd issue related to symlinking in our Greenwood monorepo when building the website
+ * and managing packages that we create as "virtual" modules, like for the mpa router
+ *
+ * ex. import @greenwood/router/router.js -> /node_modules/@greenwood/cli/src/lib/router.js
+ *
+ * when running our tests, which better emulates a real user
+ * facadeModuleId will be in node_modules, which is like how it would be for a user:
+ * /node_modules/@greenwood/cli/src/lib/router.js
+ *
+ * however, when building our website, where symlinking points back to our packages/ directory
+ * facadeModuleId will look like this:
+ * //greenwood/packages/cli/src/lib/router.js
+ *
+ * so we need to massage facadeModuleId a bit for Rollup for our internal development
+ * pathToMatch (before): /node_modules/@greenwood/cli/src/lib/router.js
+ * pathToMatch (after): /cli/src/lib/router.js
+ */
+ if (facadeModuleId && resourceKey.indexOf('/node_modules/@greenwood/cli') > 0 && facadeModuleId.indexOf('/packages/cli') > 0 && fs.existsSync(facadeModuleId)) {
+ facadeModuleId = facadeModuleId.replace('/packages/cli', '/node_modules/@greenwood/cli');
}
- });
- }));
- },
- // crawl through all entry HTML files and map Rollup bundled JavaScript and CSS filenames
- // back to their original
- if (!isRemoteUrl(parsedAttributes.src) && parsedAttributes.src) {
- for (const innerBundleId of Object.keys(bundles)) {
- const { src } = parsedAttributes;
- const facadeModuleId = bundles[innerBundleId].facadeModuleId
- ? bundles[innerBundleId].facadeModuleId.replace(/\\/g, '/')
- : bundles[innerBundleId].facadeModuleId;
- let pathToMatch = src.replace(/\.\.\//g, '').replace('./', '');
-
- /*
- * this is an odd issue related to symlinking in our Greenwood monorepo when building the website
- * and managing packages that we create as "virtaul" modules, like for the mpa router
- *
- * ex. import @greenwood/router/router.js -> /node_modules/@greenwood/cli/src/lib/router.js
- *
- * when running our tests, which better emulates a real user
- * facadeModuleId will be in node_modules, which is like how it would be for a user:
- * /node_modules/@greenwood/cli/src/lib/router.js
- *
- * however, when building our website, where symlinking points back to our packages/ directory
- * facadeModuleId will look like this:
- * //greenwood/packages/cli/src/lib/router.js
- *
- * so we need to massage pathToMatch a bit for Rollup for our internal development
- * pathToMatch (before): /node_modules/@greenwood/cli/src/lib/router.js
- * pathToMatch (after): /cli/src/lib/router.js
- */
- if (facadeModuleId && facadeModuleId.indexOf(tokenNodeModules) < 0 && fs.existsSync(path.join(projectDirectory, pathToMatch))) {
- pathToMatch = pathToMatch.replace(/\/node_modules\/@greenwood\//, '/');
- }
-
- if (facadeModuleId && facadeModuleId.indexOf(pathToMatch) > 0) {
- const newSrc = `/${innerBundleId}`;
-
- newHtml = newHtml.replace(src, newSrc);
-
- if (!parsedAttributes['data-gwd-opt'] && optimization === 'default') {
- newHtml = newHtml.replace('', `
-
-
- `);
- }
- } else if ((parsedAttributes['data-gwd-opt'] === 'static' || optimization === 'static') && newHtml.indexOf(pathToMatch) > 0) {
- newHtml = newHtml.replace(scriptTag, '');
- }
- }
- }
- });
-
- headLinks.forEach((linkTag) => {
- const parsedAttributes = parseTagForAttributes(linkTag);
- const { href } = parsedAttributes;
-
- // handle
- if (parsedAttributes.rel === 'stylesheet') {
- for (const bundleId2 of Object.keys(bundles)) {
- if (bundleId2.indexOf('.css') > 0) {
- const bundle2 = bundles[bundleId2];
- if (href.indexOf(bundle2.name) >= 0) {
- const newHref = `/${bundle2.fileName}`;
-
- newHtml = newHtml.replace(href, newHref);
-
- if (!parsedAttributes['data-gwd-opt'] && (optimization !== 'none' && optimization !== 'inline')) {
- newHtml = newHtml.replace('', `
-
-
- `);
- }
- }
- }
- }
- }
- });
-
- bundle.fileName = bundle.facadeModuleId.replace('.greenwood', 'public');
- bundle.code = newHtml;
- }
- } catch (e) {
- console.error('ERROR', e);
- }
- }
- },
-
- // crawl through all entry HTML files and map bundled JavaScript and CSS filenames
- // back to their original _inline_
- if ((parsedAttributes['data-gwd-opt'] === 'inline' || optimization === 'inline') && isScriptSrcTag && !isRemoteUrl(parsedAttributes.src)) {
- const src = parsedAttributes.src;
- const basePath = src.indexOf(tokenNodeModules) >= 0
- ? process.cwd()
- : outputDir;
- const outputPath = path.join(basePath, src);
- const js = fs.readFileSync(outputPath, 'utf-8')
- .replace(/\$/g, '$$$') // https://github.com/ProjectEvergreen/greenwood/issues/656
- .replace(/\.\//g, '/'); // force absolute paths
- scratchFiles[src] = true;
-
- html = html.replace(``, `
-
- `);
- }
-
- // handle
- if (parsedAttributes.type === 'module' && !parsedAttributes.src) {
- const id = hashString(scriptTag.rawText);
- const markerExp = `console.log\\("[0-9]+-${tokenSuffix}"\\)`;
- const markerRegex = new RegExp(markerExp);
-
- for (const innerBundleId of Object.keys(bundles)) {
- if (innerBundleId.indexOf(`-${tokenSuffix}`) > 0 && path.extname(innerBundleId) === '.js') {
- const bundledSource = fs.readFileSync(path.join(outputDir, innerBundleId), 'utf-8')
- .replace(/\$/g, '$$$') // https://github.com/ProjectEvergreen/greenwood/issues/656
- .replace(/\.\//g, '/'); // force absolute paths
-
- if (markerRegex.test(bundledSource)) {
- const marker = bundledSource.match(new RegExp(`[0-9]+-${tokenSuffix}`))[0].split('-')[0];
-
- if (id === marker) {
- if (parsedAttributes['data-gwd-opt'] === 'static') {
- html = html.replace(scriptTag.rawText, '').replace(/ or
+ sourcePathURL, // src as a URL
+ type,
+ contents,
+ optimizedFileName: undefined,
+ optimizedFileContents: undefined,
+ optimizationAttr,
+ rawAttributes
+ };
+}
+
+export { modelResource };
\ No newline at end of file
diff --git a/packages/cli/src/lib/ssr-route-worker.js b/packages/cli/src/lib/ssr-route-worker.js
index 3b8f62f14..129fecc45 100644
--- a/packages/cli/src/lib/ssr-route-worker.js
+++ b/packages/cli/src/lib/ssr-route-worker.js
@@ -1,6 +1,6 @@
// https://github.com/nodejs/modules/issues/307#issuecomment-858729422
import { pathToFileURL } from 'url';
-import { workerData, parentPort } from 'worker_threads';
+import { parentPort } from 'worker_threads';
import { renderToString, renderFromHTML } from 'wc-compiler';
async function executeRouteModule({ modulePath, compilation, route, label, id, prerender, htmlContents, scripts }) {
@@ -43,4 +43,6 @@ async function executeRouteModule({ modulePath, compilation, route, label, id, p
parentPort.postMessage(data);
}
-executeRouteModule(workerData);
\ No newline at end of file
+parentPort.on('message', async (task) => {
+ await executeRouteModule(task);
+});
\ No newline at end of file
diff --git a/packages/cli/src/lib/threadpool.js b/packages/cli/src/lib/threadpool.js
new file mode 100644
index 000000000..352aa4a39
--- /dev/null
+++ b/packages/cli/src/lib/threadpool.js
@@ -0,0 +1,79 @@
+// https://amagiacademy.com/blog/posts/2021-04-09/node-worker-threads-pool
+import { AsyncResource } from 'async_hooks';
+import { EventEmitter } from 'events';
+import { Worker } from 'worker_threads';
+
+const kTaskInfo = Symbol('kTaskInfo');
+const kWorkerFreedEvent = Symbol('kWorkerFreedEvent');
+
+class WorkerPoolTaskInfo extends AsyncResource {
+ constructor(callback) {
+ super('WorkerPoolTaskInfo');
+ this.callback = callback;
+ }
+
+ done(err, result) {
+ this.runInAsyncScope(this.callback, null, err, result);
+ this.emitDestroy();
+ }
+}
+
+class WorkerPool extends EventEmitter {
+ constructor(numThreads, workerFile) {
+ super();
+ this.numThreads = numThreads;
+ this.workerFile = workerFile;
+ this.workers = [];
+ this.freeWorkers = [];
+
+ for (let i = 0; i < numThreads; i += 1) {
+ this.addNewWorker();
+ }
+ }
+
+ addNewWorker() {
+ const worker = new Worker(this.workerFile);
+
+ worker.on('message', (result) => {
+ worker[kTaskInfo].done(null, result);
+ worker[kTaskInfo] = null;
+
+ this.freeWorkers.push(worker);
+ this.emit(kWorkerFreedEvent);
+ });
+
+ worker.on('error', (err) => {
+ if (worker[kTaskInfo]) {
+ worker[kTaskInfo].done(err, null);
+ } else {
+ this.emit('error', err);
+ }
+ this.workers.splice(this.workers.indexOf(worker), 1);
+ this.addNewWorker();
+ });
+
+ this.workers.push(worker);
+ this.freeWorkers.push(worker);
+ this.emit(kWorkerFreedEvent);
+ }
+
+ runTask(task, callback) {
+ if (this.freeWorkers.length === 0) {
+ this.once(kWorkerFreedEvent, () => this.runTask(task, callback));
+ return;
+ }
+
+ const worker = this.freeWorkers.pop();
+
+ worker[kTaskInfo] = new WorkerPoolTaskInfo(callback);
+ worker.postMessage(task);
+ }
+
+ close() {
+ for (const worker of this.workers) {
+ worker.terminate();
+ }
+ }
+}
+
+export { WorkerPool };
\ No newline at end of file
diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js
index 84d1a99b8..39d4de559 100644
--- a/packages/cli/src/lifecycles/bundle.js
+++ b/packages/cli/src/lifecycles/bundle.js
@@ -1,21 +1,159 @@
+import fs from 'fs';
import { getRollupConfig } from '../config/rollup.config.js';
+import { hashString } from '../lib/hashing-utils.js';
+import path from 'path';
import { rollup } from 'rollup';
+async function cleanUpResources(compilation) {
+ const { outputDir } = compilation.context;
+
+ for (const resource of compilation.resources.values()) {
+ const { src, optimizedFileName, optimizationAttr } = resource;
+ const optConfig = ['inline', 'static'].indexOf(compilation.config.optimization) >= 0;
+ const optAttr = ['inline', 'static'].indexOf(optimizationAttr) >= 0;
+
+ if (optimizedFileName && (!src || (optAttr || optConfig))) {
+ fs.unlinkSync(path.join(outputDir, optimizedFileName));
+ }
+ }
+}
+
+async function optimizeStaticPages(compilation, optimizeResources) {
+ const { scratchDir, outputDir } = compilation.context;
+
+ return Promise.all(compilation.graph
+ .filter(page => !page.isSSR || (page.isSSR && page.data.static) || (page.isSSR && compilation.config.prerender))
+ .map(async (page) => {
+ const { route, outputPath } = page;
+ const html = await fs.promises.readFile(path.join(scratchDir, outputPath), 'utf-8');
+
+ if (route !== '/404/' && !fs.existsSync(path.join(outputDir, route))) {
+ fs.mkdirSync(path.join(outputDir, route), {
+ recursive: true
+ });
+ }
+
+ let htmlOptimized = await optimizeResources.reduce(async (htmlPromise, resource) => {
+ const contents = await htmlPromise;
+ const shouldOptimize = await resource.shouldOptimize(outputPath, contents);
+
+ return shouldOptimize
+ ? resource.optimize(outputPath, contents)
+ : Promise.resolve(contents);
+ }, Promise.resolve(html));
+
+ // clean up optimization markers
+ htmlOptimized = htmlOptimized.replace(/data-gwd-opt=".*[a-z]"/g, '');
+
+ await fs.promises.writeFile(path.join(outputDir, outputPath), htmlOptimized);
+ })
+ );
+}
+
+async function bundleStyleResources(compilation, optimizationPlugins) {
+ const { outputDir } = compilation.context;
+
+ for (const resource of compilation.resources.values()) {
+ const { contents, optimizationAttr, src = '', type } = resource;
+
+ if (['style', 'link'].includes(type)) {
+ const resourceKey = resource.sourcePathURL.pathname;
+ const srcPath = src && src.replace(/\.\.\//g, '').replace('./', '');
+ let optimizedFileName;
+ let optimizedFileContents;
+
+ if (src) {
+ const basename = path.basename(srcPath);
+ const basenamePieces = path.basename(srcPath).split('.');
+ const fileNamePieces = srcPath.split('/').filter(piece => piece !== ''); // normalize by removing any leading /'s
+
+ optimizedFileName = srcPath.indexOf('/node_modules') >= 0
+ ? `${basenamePieces[0]}.${hashString(contents)}.css`
+ : fileNamePieces.join('/').replace(basename, `${basenamePieces[0]}.${hashString(contents)}.css`);
+ } else {
+ optimizedFileName = `${hashString(contents)}.css`;
+ }
+
+ const outputPathRoot = path.join(outputDir, path.dirname(optimizedFileName));
+
+ if (!fs.existsSync(outputPathRoot)) {
+ fs.mkdirSync(outputPathRoot, {
+ recursive: true
+ });
+ }
+
+ if (compilation.config.optimization === 'none' || optimizationAttr === 'none') {
+ optimizedFileContents = contents;
+ } else {
+ const url = resource.sourcePathURL.pathname;
+ let optimizedStyles = await fs.promises.readFile(url, 'utf-8');
+
+ for (const plugin of optimizationPlugins) {
+ optimizedStyles = await plugin.shouldIntercept(url, optimizedStyles)
+ ? (await plugin.intercept(url, optimizedStyles)).body
+ : optimizedStyles;
+ }
+
+ for (const plugin of optimizationPlugins) {
+ optimizedStyles = await plugin.shouldOptimize(url, optimizedStyles)
+ ? await plugin.optimize(url, optimizedStyles)
+ : optimizedStyles;
+ }
+
+ optimizedFileContents = optimizedStyles;
+ }
+
+ compilation.resources.set(resourceKey, {
+ ...compilation.resources.get(resourceKey),
+ optimizedFileName,
+ optimizedFileContents
+ });
+
+ await fs.promises.writeFile(path.join(outputDir, optimizedFileName), optimizedFileContents);
+ }
+ }
+}
+
+async function bundleScriptResources(compilation) {
+ // https://rollupjs.org/guide/en/#differences-to-the-javascript-api
+ const [rollupConfig] = await getRollupConfig(compilation);
+
+ if (rollupConfig.input.length !== 0) {
+ const bundle = await rollup(rollupConfig);
+ await bundle.write(rollupConfig.output);
+ }
+}
+
const bundleCompilation = async (compilation) => {
return new Promise(async (resolve, reject) => {
try {
- compilation.graph = compilation.graph.filter(page => !page.isSSR || (page.isSSR && page.data.static) || (page.isSSR && compilation.config.prerender));
+ const optimizeResourcePlugins = compilation.config.plugins.filter((plugin) => {
+ return plugin.type === 'resource';
+ }).map((plugin) => {
+ return plugin.provider(compilation);
+ }).filter((provider) => {
+ return provider.shouldIntercept && provider.intercept
+ || provider.shouldOptimize && provider.optimize;
+ });
+ // centrally register all static resources
+ compilation.graph.map((page) => {
+ return page.imports;
+ }).flat().forEach(resource => {
+ compilation.resources.set(resource.sourcePathURL.pathname, resource);
+ });
- // https://rollupjs.org/guide/en/#differences-to-the-javascript-api
- if (compilation.graph.length > 0) {
- const rollupConfigs = await getRollupConfig({
- ...compilation
- });
- const bundle = await rollup(rollupConfigs[0]);
+ console.info('bundling static assets...');
- await bundle.write(rollupConfigs[0].output);
- }
+ await Promise.all([
+ await bundleScriptResources(compilation),
+ await bundleStyleResources(compilation, optimizeResourcePlugins.filter(plugin => plugin.contentType.includes('text/css')))
+ ]);
+
+ console.info('optimizing static pages....');
+
+ await optimizeStaticPages(compilation, optimizeResourcePlugins.filter(plugin => plugin.contentType.includes('text/html')));
+ await cleanUpResources(compilation);
resolve();
} catch (err) {
diff --git a/packages/cli/src/lifecycles/compile.js b/packages/cli/src/lifecycles/compile.js
index 698d73435..a8d02f83a 100644
--- a/packages/cli/src/lifecycles/compile.js
+++ b/packages/cli/src/lifecycles/compile.js
@@ -9,7 +9,8 @@ const generateCompilation = () => {
let compilation = {
graph: [],
context: {},
- config: {}
+ config: {},
+ resources: new Map()
};
console.info('Initializing project config');
diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js
index 53ccbb06b..1e4fbdaf8 100644
--- a/packages/cli/src/lifecycles/config.js
+++ b/packages/cli/src/lifecycles/config.js
@@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL, URL } from 'url';
// get and "tag" all plugins provided / maintained by the @greenwood/cli
// and include as the default set, with all user plugins getting appended
const greenwoodPluginsBasePath = fileURLToPath(new URL('../plugins', import.meta.url));
+const PLUGINS_FLATTENED_DEPTH = 2;
const greenwoodPlugins = (await Promise.all([
path.join(greenwoodPluginsBasePath, 'copy'),
@@ -14,7 +15,7 @@ const greenwoodPlugins = (await Promise.all([
].map(async (pluginDirectory) => {
const files = await fs.promises.readdir(pluginDirectory);
- return (await Promise.all(files.map(async(file) => {
+ return await Promise.all(files.map(async(file) => {
const importPaTh = pathToFileURL(`${pluginDirectory}${path.sep}${file}`);
const pluginImport = await import(importPaTh);
const plugin = pluginImport[Object.keys(pluginImport)[0]];
@@ -22,8 +23,8 @@ const greenwoodPlugins = (await Promise.all([
return Array.isArray(plugin)
? plugin
: [plugin];
- }))).flat();
-}).flat())).flat().map((plugin) => {
+ }));
+}))).flat(PLUGINS_FLATTENED_DEPTH).map((plugin) => {
return {
isGreenwoodDefaultPlugin: true,
...plugin
@@ -99,7 +100,9 @@ const readAndMergeConfig = async() => {
}
if (plugins && plugins.length > 0) {
- plugins.forEach(plugin => {
+ const flattened = plugins.flat(PLUGINS_FLATTENED_DEPTH);
+
+ flattened.forEach(plugin => {
if (!plugin.type || pluginTypes.indexOf(plugin.type) < 0) {
reject(`Error: greenwood.config.js plugins must be one of type "${pluginTypes.join(', ')}". got "${plugin.type}" instead.`);
}
@@ -117,14 +120,22 @@ const readAndMergeConfig = async() => {
}
});
- // if user provides a custom renderer, replace ours with theirs
- if (plugins.filter(plugin => plugin.type === 'renderer').length === 1) {
+ // if user provided a custom renderer, filter out Greenwood's default renderer
+ const customRendererPlugins = flattened.filter(plugin => plugin.type === 'renderer').length;
+
+ if (customRendererPlugins === 1) {
customConfig.plugins = customConfig.plugins.filter((plugin) => {
return plugin.type !== 'renderer';
});
+ } else if (customRendererPlugins > 1) {
+ console.warn('More than one custom renderer plugin detected. Please make sure you are only loading one.');
+ console.debug(plugins.filter(plugin => plugin.type === 'renderer'));
}
- customConfig.plugins = customConfig.plugins.concat(plugins);
+ customConfig.plugins = [
+ ...customConfig.plugins,
+ ...flattened
+ ];
}
if (devServer && Object.keys(devServer).length > 0) {
diff --git a/packages/cli/src/lifecycles/context.js b/packages/cli/src/lifecycles/context.js
index 2751882fe..18589faa9 100644
--- a/packages/cli/src/lifecycles/context.js
+++ b/packages/cli/src/lifecycles/context.js
@@ -3,7 +3,7 @@ import path from 'path';
import { fileURLToPath, URL } from 'url';
const initContext = async({ config }) => {
- const scratchDir = path.join(process.cwd(), './.greenwood/');
+ const scratchDir = path.join(process.cwd(), './.greenwood');
const outputDir = path.join(process.cwd(), './public');
const dataDir = fileURLToPath(new URL('../data', import.meta.url));
diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js
index d9fe666d5..2f6381668 100644
--- a/packages/cli/src/lifecycles/graph.js
+++ b/packages/cli/src/lifecycles/graph.js
@@ -1,6 +1,7 @@
/* eslint-disable complexity, max-depth */
import fs from 'fs';
import fm from 'front-matter';
+import { modelResource } from '../lib/resource-utils.js';
import path from 'path';
import toc from 'markdown-toc';
import { Worker } from 'worker_threads';
@@ -118,21 +119,23 @@ const generateGraph = async (compilation) => {
}
/* ---------End Menu Query-------------------- */
} else if (isDynamic) {
- const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider().workerUrl;
+ const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation).workerUrl;
let ssrFrontmatter;
filePath = route;
await new Promise((resolve, reject) => {
- const worker = new Worker(routeWorkerUrl, {
- workerData: {
- modulePath: fullPath,
- compilation: JSON.stringify(compilation),
- route
- }
- });
+ const worker = new Worker(routeWorkerUrl);
+
worker.on('message', (result) => {
if (result.frontmatter) {
+ const resources = (result.frontmatter.imports || []).map((resource) => {
+ const type = path.extname(resource) === '.js' ? 'script' : 'link';
+
+ return modelResource(compilation.context, type, resource);
+ });
+
+ result.frontmatter.imports = resources;
ssrFrontmatter = result.frontmatter;
}
resolve();
@@ -143,6 +146,12 @@ const generateGraph = async (compilation) => {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
+
+ worker.postMessage({
+ modulePath: fullPath,
+ compilation: JSON.stringify(compilation),
+ route
+ });
});
if (ssrFrontmatter) {
diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js
index cec3591fa..6ba752330 100644
--- a/packages/cli/src/lifecycles/prerender.js
+++ b/packages/cli/src/lifecycles/prerender.js
@@ -1,8 +1,76 @@
import fs from 'fs';
import htmlparser from 'node-html-parser';
+import { modelResource } from '../lib/resource-utils.js';
+import os from 'os';
import path from 'path';
-import { Worker } from 'worker_threads';
-import { pathToFileURL } from 'url';
+import { WorkerPool } from '../lib/threadpool.js';
+
+function isLocalLink(url = '') {
+ return url !== '' && (url.indexOf('http') !== 0 && url.indexOf('//') !== 0);
+}
+
+function createOutputDirectory(route, outputPathDir) {
+ if (route !== '/404/' && !fs.existsSync(outputPathDir)) {
+ fs.mkdirSync(outputPathDir, {
+ recursive: true
+ });
+ }
+}
+
+// TODO does this make more sense in bundle lifecycle?
+// https://github.com/ProjectEvergreen/greenwood/issues/970
+// or could this be done sooner (like in appTemplate building in html resource plugin)?
+// Or do we need to ensure userland code / plugins have gone first
+// before we can curate the final list of
+ return modelResource(context, 'script', src, null, optimizationAttr, rawAttrs);
+ } else if (script.rawText) {
+ //
+ return modelResource(context, 'script', null, script.rawText, optimizationAttr, rawAttrs);
+ }
+ });
+
+ const styles = root.querySelectorAll('head style')
+ .filter(style => !(/\$/).test(style.rawText) && !(//).test(style.rawText)) // filter out Shady DOM
- `;
- });
-
- if (lastStyle === '') {
- appTemplateContents = appTemplateContents.replace(lastStyle, `
- ${pageBodyStyles.join('\n')}
- ${lastStyle}\n
- `);
- } else {
- appTemplateContents = appTemplateContents.replace(lastStyle, `${lastStyle}\n
- ${pageBodyStyles.join('\n')}
- `);
- }
- }
-
- // merge
- if (headMeta.length > 0) {
- const matchNeedleMeta = / 0
- ? appHeadMetaMatches[appHeadMetaMatches.length - 1]
- : '';
- const pageMeta = headMeta.map((meta) => {
- return ``;
- });
-
- appTemplateContents = appTemplateContents.replace(lastMeta.replace('>', '/>'), `${lastMeta.replace('>', '/>')}
- ${pageMeta.join('\n')}
- `);
- }
-
- customImports.forEach((customImport) => {
- const extension = path.extname(customImport);
- switch (extension) {
-
- case '.js':
- appTemplateContents = appTemplateContents.replace('', `
-
-
- `);
- break;
- case '.css':
- appTemplateContents = appTemplateContents.replace('', `
-
-
- `);
- break;
-
- default:
- break;
-
- }
- });
+ const mergedHtml = pageRoot.querySelector('html').rawAttrs !== ''
+ ? ``
+ : appRoot.querySelector('html').rawAttrs !== ''
+ ? ``
+ : '';
+
+ const mergedMeta = [
+ ...appRoot.querySelectorAll('head meta'),
+ ...pageRoot.querySelectorAll('head meta')
+ ].join('\n');
+
+ const mergedLinks = [
+ ...appRoot.querySelectorAll('head link'),
+ ...pageRoot.querySelectorAll('head link')
+ ].join('\n');
+
+ const mergedStyles = [
+ ...appRoot.querySelectorAll('head style'),
+ ...pageRoot.querySelectorAll('head style'),
+ ...customImports.filter(resource => path.extname(resource) === '.css')
+ .map(resource => ``)
+ ].join('\n');
+
+ const mergedScripts = [
+ ...appRoot.querySelectorAll('head script'),
+ ...pageRoot.querySelectorAll('head script'),
+ ...customImports.filter(resource => path.extname(resource) === '.js')
+ .map(resource => ``)
+ ].join('\n');
+
+ mergedTemplateContents = `
+ ${mergedHtml}
+
+ ${title}
+ ${mergedMeta}
+ ${mergedLinks}
+ ${mergedStyles}
+ ${mergedScripts}
+
+
+ ${appBody.replace(/<\/page-outlet>/, pageBody)}
+
+
+ `;
}
- return appTemplateContents;
+ return mergedTemplateContents;
};
const getUserScripts = (contents, context) => {
@@ -387,13 +294,8 @@ class StandardHtmlResource extends ResourceInterface {
const routeWorkerUrl = this.compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider().workerUrl;
await new Promise((resolve, reject) => {
- const worker = new Worker(routeWorkerUrl, {
- workerData: {
- modulePath: routeModuleLocation,
- compilation: JSON.stringify(this.compilation),
- route: fullPath
- }
- });
+ const worker = new Worker(routeWorkerUrl);
+
worker.on('message', (result) => {
if (result.template) {
ssrTemplate = result.template;
@@ -425,6 +327,12 @@ class StandardHtmlResource extends ResourceInterface {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
+
+ worker.postMessage({
+ modulePath: routeModuleLocation,
+ compilation: JSON.stringify(this.compilation),
+ route: fullPath
+ });
});
}
@@ -497,21 +405,79 @@ class StandardHtmlResource extends ResourceInterface {
}
async optimize(url, body) {
+ const { optimization } = this.compilation.config;
+ const pageResources = this.compilation.graph.find(page => page.outputPath === url || page.route === url).imports;
+
return new Promise((resolve, reject) => {
try {
- const hasHead = body.match(/\(.*)<\/head>/s);
-
- if (hasHead && hasHead.length > 0) {
- let contents = hasHead[0];
-
- contents = contents.replace(/`, `
+
+ `);
+ } else if (optimizationAttr === 'static' || optimization === 'static') {
+ body = body.replace(``, '');
+ }
+ } else if (type === 'link') {
+ if (!optimizationAttr && (optimization !== 'none' && optimization !== 'inline')) {
+ const optimizedFilePath = `/${optimizedFileName}`;
+
+ body = body.replace(src, optimizedFilePath);
+ body = body.replace('', `
+
+
+ `);
+ } else if (optimizationAttr === 'inline' || optimization === 'inline') {
+ // https://github.com/ProjectEvergreen/greenwood/issues/810
+ // when pre-rendering, puppeteer normalizes everything to
+ // but if not using pre-rendering, then it could come out as
+ // not great, but best we can do for now until #742
+ body = body.replace(``, `
+
+ `).replace(``, `
+
+ `);
+ }
+ }
+ } else {
+ if (type === 'script') {
+ if (optimizationAttr === 'static' || optimization === 'static') {
+ body = body.replace(``, '');
+ } else if (optimizationAttr === 'none') {
+ body = body.replace(contents, contents.replace(/\.\//g, '/').replace(/\$/g, '$$$'));
+ } else {
+ body = body.replace(contents, optimizedFileContents.replace(/\.\//g, '/').replace(/\$/g, '$$$'));
+ }
+ } else if (type === 'style') {
+ body = body.replace(contents, optimizedFileContents);
+ }
+ }
}
+ // TODO clean up lit-polyfill as part of https://github.com/ProjectEvergreen/greenwood/issues/728
+ body = body.replace(/\n
+
+ `);
+
+ resolve({ body });
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
+
+ async shouldOptimize(url, body, headers = { request: {} }) {
+ const contentType = headers.request['content-type'];
+
return Promise.resolve(this.compilation.config.staticRouter
&& url !== '404.html'
- && (path.extname(url) === '.html' || (headers.request && headers.request['content-type'].indexOf('text/html') >= 0)));
+ && (path.extname(url) === '.html' || (contentType && contentType.indexOf('text/html') >= 0)));
}
async optimize(url, body) {
return new Promise(async (resolve, reject) => {
try {
let currentTemplate;
- const isStaticRoute = path.extname(url) === '.html';
- const { projectDirectory, scratchDir, outputDir } = this.compilation.context;
+ const isStaticRoute = this.compilation.graph.filter(page => page.outputPath === url && url !== '/404/' && !page.isSSR).length === 1;
+ const { outputDir } = this.compilation.context;
const bodyContents = body.match(/(.*)<\/body>/s)[0].replace('', '').replace('', '');
- const outputBundlePath = path.normalize(`${outputDir}/_routes${url.replace(projectDirectory, '')}`)
- .replace(`.greenwood${path.sep}`, '');
+ const outputBundlePath = path.join(`${outputDir}/_routes${url}`);
const routeTags = this.compilation.graph
.filter(page => !page.isSSR)
@@ -61,7 +86,7 @@ class StaticRouterResource extends ResourceInterface {
? ''
: page.route.slice(0, page.route.lastIndexOf('/'));
- if (url.replace(scratchDir, '') === `${page.route}index.html`) {
+ if (url === page.outputPath) {
currentTemplate = template;
}
return `
@@ -80,22 +105,22 @@ class StaticRouterResource extends ResourceInterface {
}
body = body.replace('', `
- \n
- `).replace(/(.*)<\/body>/s, `
- \n
+ `.replace(/\n/g, '').replace(/ /g, ''))
+ .replace(/(.*)<\/body>/s, `
+ \n
-
- ${bodyContents.replace(/\$/g, '$$$')}\n
-
+
+ ${bodyContents.replace(/\$/g, '$$$')}\n
+
- ${routeTags.join('\n')}
-
- `);
+ ${routeTags.join('\n')}
+
+ `);
resolve(body);
} catch (e) {
diff --git a/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js b/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js
index e4209e148..2d7f9eefb 100644
--- a/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js
+++ b/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js
@@ -64,13 +64,13 @@ describe('Build Greenwood With: ', function() {
expect(authorMeta).to.be.equal('Owen Buckley');
});
- it('should have the correct value for publised in the
tag', function() {
+ it('should have the correct value for published in the
tag', function() {
const heading = dom.window.document.querySelector('body h3').textContent;
expect(heading).to.be.equal('Published: 11/11/2022');
});
- it('should have the correct value for authro in the
tag', function() {
+ it('should have the correct value for author in the