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

tag', function() { const heading = dom.window.document.querySelector('body h4').textContent; expect(heading).to.be.equal('Author: Owen Buckley'); diff --git a/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js b/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js index 39c74c5a7..713bde998 100644 --- a/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js +++ b/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js @@ -20,6 +20,7 @@ * theme.css */ import chai from 'chai'; +import fs from 'fs'; import glob from 'glob-promise'; import { JSDOM } from 'jsdom'; import path from 'path'; @@ -33,6 +34,7 @@ describe('Build Greenwood With: ', function() { const LABEL = 'Default Optimization Configuration'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const expectedCss = fs.readFileSync(path.join(outputPath, './fixtures/expected.css'), 'utf-8').replace(/\n/g, ''); let runner; before(function() { @@ -57,7 +59,7 @@ describe('Build Greenwood With: ', function() { }); describe(' - + diff --git a/packages/cli/test/cases/build.config.optimization-default/src/styles/main.css b/packages/cli/test/cases/build.config.optimization-default/src/styles/main.css new file mode 100644 index 000000000..a0868cae7 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-default/src/styles/main.css @@ -0,0 +1,122 @@ +@import './theme.css'; + +* { + margin: 0; + padding: 0; + font-family: 'Comic Sans', sans-serif; +} + +body { + background-color: green; +} + +h1, h2 { + color: var(--primary-color); + border: 0.5px solid #dddde1; +} + +#foo, .bar { + color: var(--secondary-color); +} + +div > p { + display: none; +} + +a[title] { + color: purple; +} + +@media screen and (max-width: 992px) { + body { + background-color: blue; + } +} + +p::first-line { + color: blue; + width: 100%!important; +} + +pre[class*="language-"] { + color: #ccc; + background: none; +} + +dd:only-of-type { + background-color: bisque; +} + +:not(pre) > code[class*="language-"] { + background: #2d2d2d; +} + +li:nth-child(-n+3) { + border: 2px solid orange; + margin-bottom: 1px; +} + +li:nth-child(even) { + background-color: lightyellow; +} + +li:nth-last-child(5n) { + border: 2px solid orange; + margin-top: 1px; +} + +dd:nth-last-of-type(odd) { + border: 2px solid orange; +} + +p:nth-of-type(2n + 1) { + color: red; +} + +*:lang(en-US) { + outline: 2px solid deeppink; +} + +p ~ ul { + font-weight: bold; +} + +a[href*="greenwood"], a[href$=".pdf"] { + color: orange; +} + +[title~=flower], a[href^="https"], [lang|=en] { + text-decoration: underline; +} + +@keyframes slidein { + from { + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } +} + +@supports (display: flex) { + .flex-container > * { + text-shadow: 0px 0px 2px blue; + float: none; + } + + .flex-container { + display: flex; + } +} + +@page { + size: 8.5in 9in; + margin-top: 4in; +} + +@font-feature-values Font One { + @styleset { + nice-style: 12; + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-default/src/styles/theme.css b/packages/cli/test/cases/build.config.optimization-default/src/styles/theme.css index 2f3a9deff..120e7130c 100644 --- a/packages/cli/test/cases/build.config.optimization-default/src/styles/theme.css +++ b/packages/cli/test/cases/build.config.optimization-default/src/styles/theme.css @@ -1,5 +1,6 @@ -* { - margin: 0; - padding: 0; - font-family: 'Comic Sans', sans-serif; -} \ No newline at end of file +@tailwind base; +@tailwind components; +@tailwind utilities; + +@import url('../system/variables.css'); +@import url('https://fonts.googleapis.com/css?family=Raleway&display=swap'); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-default/src/system/variables.css b/packages/cli/test/cases/build.config.optimization-default/src/system/variables.css new file mode 100644 index 000000000..f010eba84 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-default/src/system/variables.css @@ -0,0 +1,16 @@ +:root, :host { + --primary-color: #16f; + --secondary-color: #ff7; +} + +/* source-sans-pro-regular - latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), + url('/assets/fonts/source-sans-pro-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('/assets/fonts/source-sans-pro-v13-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('/assets/fonts/source-sans-pro-v13-latin-regular.ttf') format('truetype'); /* Safari, Android, iOS */ +} \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js b/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js index a882c1418..789440089 100644 --- a/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js +++ b/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js @@ -138,7 +138,7 @@ describe('Build Greenwood With: ', function() { it('should contain the expected CSS content inlined for theme.css', function() { const styleTags = dom.window.document.querySelectorAll('head style'); - expect(styleTags[0].textContent).to.contain('*{font-family:Comic Sans,sans-serif;margin:0;padding:0}'); + expect(styleTags[0].textContent).to.contain('*{margin:0;padding:0;font-family:\'Comic Sans\',sans-serif;}'); }); it('should contain the expected CSS content inlined for page.css', function() { diff --git a/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js b/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js index 7eb1f0dcb..8b1ae1b94 100644 --- a/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js +++ b/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js @@ -71,15 +71,15 @@ describe('Build Greenwood With: ', function() { }); describe(' tag in the ', function() { + it('should have one - + @@ -60,6 +60,13 @@

CSS output two + + + \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/popup.js b/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/popup.js new file mode 100644 index 000000000..f0acde54b --- /dev/null +++ b/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/popup.js @@ -0,0 +1 @@ +alert('surprise!'); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js b/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js index 46ee90899..074c04c8d 100644 --- a/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js +++ b/packages/cli/test/cases/build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js @@ -90,15 +90,16 @@ describe('Build Greenwood With: ', function() { expect(linkTags.length).to.equal(4); }); - it('should have 5 + + + `; + } +} + +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-build.prerender/src/pages/index.md b/packages/plugin-import-css/test/cases/exp-build.prerender/src/pages/index.md new file mode 100644 index 000000000..82c330a89 --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-build.prerender/src/pages/index.md @@ -0,0 +1,3 @@ +# Home Page + +Welcome to the home page! \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-build.prerender/src/templates/app.html b/packages/plugin-import-css/test/cases/exp-build.prerender/src/templates/app.html new file mode 100644 index 000000000..7c394fe4b --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-build.prerender/src/templates/app.html @@ -0,0 +1,12 @@ + + + My Personal Website + + + + + + + + + \ No newline at end of file diff --git a/packages/plugin-import-json/README.md b/packages/plugin-import-json/README.md index c12064cc7..e3c396d11 100644 --- a/packages/plugin-import-json/README.md +++ b/packages/plugin-import-json/README.md @@ -5,10 +5,6 @@ A Greenwood plugin to allow you use ESM (`import`) syntax to load your JSON. > This package assumes you already have `@greenwood/cli` installed. -## Caveats - -As of now, this transformation is only supported for client side (browser) code and will not run correctly in NodeJS until [support for this is introduced into Greenwood](https://github.com/ProjectEvergreen/greenwood/issues/878), or [natively by NodeJS](https://github.com/ProjectEvergreen/greenwood/issues/957). This means it will not work when using `prerender` option with WCC. - ## Installation You can use your favorite JavaScript package manager to install this package. @@ -31,16 +27,23 @@ export default { ... plugins: [ - ...greenwoodPluginImportJson() // notice the spread ... ! + greenwoodPluginImportJson() ] } ``` -This will then allow you use `import` to include JSON in your JavaScript files by appending `?type=json` to the end of the `import` statement. - +This will then allow you to use `import` to include JSON in your JavaScript files. ```js -// { status: 200, message: 'some data' } -import json from '../path/to/data.json?type=json'; // must be a relative path per ESM spec +import json from '../path/to/data.json'; // must be a relative path per ESM spec + +console.log(json) // { status: 200, message: 'some data' } +``` + +A couple notes: +- For SSR and `prerender` use cases, [follow these steps](/docs/server-rendering/#custom-imports-experimental) +- For client side / browser code specifically, it is recommended to append `?type=json`, e.g. + ```js + import json from '../path/to/data.json?type=json'; + ``` -console.log(json.status) // 200 -``` \ No newline at end of file +> _The plan is to coalesce around [import assertions](https://github.com/ProjectEvergreen/greenwood/issues/923) in time for the v1.0 release so the same standard syntax can be used on the client and the server._ \ No newline at end of file diff --git a/packages/plugin-import-json/package.json b/packages/plugin-import-json/package.json index a96708b4a..990975d82 100644 --- a/packages/plugin-import-json/package.json +++ b/packages/plugin-import-json/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-import-json", - "version": "0.26.2", + "version": "0.27.0-alpha.7", "description": "A Greenwood plugin to allow you to use ESM (import) syntax to load your JSON.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-json", "author": "Owen Buckley ", @@ -23,10 +23,7 @@ "peerDependencies": { "@greenwood/cli": "^0.12.3" }, - "dependencies": { - "@rollup/plugin-json": "^4.1.0" - }, "devDependencies": { - "@greenwood/cli": "^0.26.2" + "@greenwood/cli": "^0.27.0-alpha.7" } } diff --git a/packages/plugin-import-json/src/index.js b/packages/plugin-import-json/src/index.js index a8fdfba28..99f0d0642 100644 --- a/packages/plugin-import-json/src/index.js +++ b/packages/plugin-import-json/src/index.js @@ -5,7 +5,6 @@ * */ import fs from 'fs'; -import pluginRollupJson from '@rollup/plugin-json'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; class ImportJsonResource extends ResourceInterface { @@ -15,20 +14,30 @@ class ImportJsonResource extends ResourceInterface { this.contentType = 'text/javascript'; } + // TODO resolve as part of https://github.com/ProjectEvergreen/greenwood/issues/952 + async shouldServe() { + return false; + } + + // TODO handle it from node_modules too, when without `?type=json` async shouldIntercept(url, body, headers) { const { originalUrl } = headers.request; + const type = this.extensions[0].replace('.', ''); - return Promise.resolve(originalUrl && originalUrl.indexOf('?type=json') >= 0); + return Promise.resolve(originalUrl && originalUrl.indexOf(`?type=${type}`) >= 0); } - async intercept(url) { + async intercept(url, body = '') { return new Promise(async (resolve, reject) => { try { - const contents = await fs.promises.readFile(url, 'utf-8'); - const body = `export default ${contents}`; + // TODO better way to handle this? + // https://github.com/ProjectEvergreen/greenwood/issues/948 + const raw = body === '' + ? await fs.promises.readFile(url, 'utf-8') + : body; resolve({ - body, + body: `export default ${JSON.stringify(raw)}`, contentType: this.contentType }); } catch (e) { @@ -42,14 +51,6 @@ const greenwoodPluginImportJson = (options = {}) => [{ type: 'resource', name: 'plugin-import-json:resource', provider: (compilation) => new ImportJsonResource(compilation, options) -}, { - type: 'rollup', - name: 'plugin-import-json:rollup', - provider: () => { - return [ - pluginRollupJson() - ]; - } }]; export { greenwoodPluginImportJson }; \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/default/default.spec.js b/packages/plugin-import-json/test/cases/default/default.spec.js index 0690e35a3..4554b8c53 100644 --- a/packages/plugin-import-json/test/cases/default/default.spec.js +++ b/packages/plugin-import-json/test/cases/default/default.spec.js @@ -72,7 +72,7 @@ describe('Build Greenwood With: ', function() { it('should have the expected output from importing data.json in main.js', function() { const contents = fs.readFileSync(scripts[0], 'utf-8'); - expect(contents).to.contain('const t=`${"got json"} via import, status is - ${200}`'); + expect(contents).to.contain('document.getElementsByClassName("output-json-import")[0].innerHTML="got json via import, status is - 200"'); }); }); }); diff --git a/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js b/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js index 025ad01da..9bb03407c 100644 --- a/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js @@ -93,8 +93,8 @@ describe('Develop Greenwood With: ', function() { done(); }); - it('should return an ECMASCript module', function(done) { - expect(response.body.replace(/\n/g, '')).to.equal('export default { "status": 200, "message": "got json"}'); + it('should return an ECMAScript module', function(done) { + expect(response.body).to.equal('export default {"status":200,"message":"got json"}'); done(); }); }); diff --git a/packages/plugin-import-json/test/cases/exp-build.prerender/exp-build.prerender.spec.js b/packages/plugin-import-json/test/cases/exp-build.prerender/exp-build.prerender.spec.js new file mode 100644 index 000000000..59f065a5e --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-build.prerender/exp-build.prerender.spec.js @@ -0,0 +1,89 @@ +/* + * Use Case + * Run Greenwood with greenwoodPluginImportJson plugin with prerendering of JSON on the server side. + * + * User Result + * Should generate a static Greenwood build with CSS properly prerendered. + * + * User Command + * greenwood build + * + * User Config + * import { greenwoodPluginImportJson } from '@greenwood/plugin-import-json'; + * + * { + * plugins: [{ + * greenwoodPluginImportJson() + * }] + * } + * + * User Workspace + * package.json + * src/ + * components/ + * footer.js + * pages/ + * index.md + * templates/ + * app.html + */ +import chai from 'chai'; +import glob from 'glob-promise'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('(Experimental) Build Greenwood With: ', function() { + const LABEL = 'Import JSON Plugin with static pre-rendering'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(async function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + before(async function() { + await runner.setup(outputPath, getSetupFiles(outputPath)); + await runner.runCommand(cliPath, 'build'); + }); + + runSmokeTest(['public'], LABEL); + + describe('importing JSON using ESM (import)', function() { + let dom; + let scripts; + + before(async function() { + scripts = await glob.promise(path.join(this.context.publicDir, '*.js')); + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should contain no (JSON-in) JavaScript files in the output directory', function() { + expect(scripts.length).to.be.equal(0); + }); + + it('should have the expected content from importing values from package.json in index.html', function() { + const headings = dom.window.document.querySelectorAll('app-footer footer h4'); + const year = new Date().getFullYear(); + + expect(headings.length).to.equal(1); + expect(headings[0].textContent.trim()).to.equal(`My Blog ${year} - Built with test-plugin-import-json-build-prerender-v0.27.0-alpha.0`); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-build.prerender/greenwood.config.js b/packages/plugin-import-json/test/cases/exp-build.prerender/greenwood.config.js new file mode 100644 index 000000000..8cd4f489a --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-build.prerender/greenwood.config.js @@ -0,0 +1,8 @@ +import { greenwoodPluginImportJson } from '../../../src/index.js'; + +export default { + prerender: true, + plugins: [ + ...greenwoodPluginImportJson() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-build.prerender/package.json b/packages/plugin-import-json/test/cases/exp-build.prerender/package.json new file mode 100644 index 000000000..e04a32e74 --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-build.prerender/package.json @@ -0,0 +1,5 @@ +{ + "name": "test-plugin-import-json-build-prerender", + "type": "module", + "version": "0.27.0-alpha.0" +} \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-build.prerender/src/components/footer.js b/packages/plugin-import-json/test/cases/exp-build.prerender/src/components/footer.js new file mode 100644 index 000000000..83b25d432 --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-build.prerender/src/components/footer.js @@ -0,0 +1,22 @@ +import packageJson from '../../package.json'; + +export default class FooterComponent extends HTMLElement { + connectedCallback() { + this.innerHTML = this.getTemplate(); + } + + getTemplate() { + const { name, version } = packageJson; + const year = new Date().getFullYear(); + + return ` +
+

+ My Blog ${year} - Built with ${name}-v${version} +

+
+ `; + } +} + +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-build.prerender/src/pages/index.md b/packages/plugin-import-json/test/cases/exp-build.prerender/src/pages/index.md new file mode 100644 index 000000000..82c330a89 --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-build.prerender/src/pages/index.md @@ -0,0 +1,3 @@ +# Home Page + +Welcome to the home page! \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-build.prerender/src/templates/app.html b/packages/plugin-import-json/test/cases/exp-build.prerender/src/templates/app.html new file mode 100644 index 000000000..7c394fe4b --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-build.prerender/src/templates/app.html @@ -0,0 +1,12 @@ + + + My Personal Website + + + + + + + + + \ No newline at end of file diff --git a/packages/plugin-include-html/README.md b/packages/plugin-include-html/README.md index 699714505..424fdd5af 100644 --- a/packages/plugin-include-html/README.md +++ b/packages/plugin-include-html/README.md @@ -15,7 +15,7 @@ export default { ... plugins: [ - ...greenwoodPluginIncludeHtml() // notice the spread ... ! + greenwoodPluginIncludeHtml() ] } ``` diff --git a/packages/plugin-include-html/package.json b/packages/plugin-include-html/package.json index 8ba3cca6c..6a45f723c 100644 --- a/packages/plugin-include-html/package.json +++ b/packages/plugin-include-html/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-include-html", - "version": "0.26.2", + "version": "0.27.0-alpha.7", "description": "A Greenwood plugin to let you render server side JS from HTML or JS at build time as HTML.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-include-html", "author": "Owen Buckley ", @@ -24,6 +24,6 @@ "@greenwood/cli": "^0.4.0" }, "devDependencies": { - "@greenwood/cli": "^0.26.2" + "@greenwood/cli": "^0.27.0-alpha.7" } } diff --git a/packages/plugin-polyfills/README.md b/packages/plugin-polyfills/README.md index 9b9ccd9f3..89b79527a 100644 --- a/packages/plugin-polyfills/README.md +++ b/packages/plugin-polyfills/README.md @@ -35,7 +35,7 @@ export default { ... plugins: [ - ...greenwoodPluginPolyfills() // notice the spread ... ! + greenwoodPluginPolyfills() ] } ``` diff --git a/packages/plugin-polyfills/package.json b/packages/plugin-polyfills/package.json index cc827ee0c..cc4fa5fcc 100644 --- a/packages/plugin-polyfills/package.json +++ b/packages/plugin-polyfills/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-polyfills", - "version": "0.26.2", + "version": "0.27.0-alpha.7", "description": "A Greenwood plugin adding support for Web Component related polyfills like Custom Elements and Shadow DOM.", "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-polyfills", "author": "Owen Buckley ", @@ -26,6 +26,6 @@ "@webcomponents/webcomponentsjs": "^2.6.0" }, "devDependencies": { - "@greenwood/cli": "^0.26.2" + "@greenwood/cli": "^0.27.0-alpha.7" } } diff --git a/packages/plugin-polyfills/src/index.js b/packages/plugin-polyfills/src/index.js index 0ce40a4e9..f45346093 100644 --- a/packages/plugin-polyfills/src/index.js +++ b/packages/plugin-polyfills/src/index.js @@ -14,11 +14,11 @@ class PolyfillsResource extends ResourceInterface { }; } - async shouldOptimize(url = '', body, headers = {}) { - return Promise.resolve(path.extname(url) === '.html' || (headers.request && headers.request['content-type'].indexOf('text/html') >= 0)); + async shouldIntercept(url, body, headers = { request: {} }) { + return Promise.resolve(headers.request['content-type'] && headers.request['content-type'].indexOf('text/html') >= 0); } - async optimize(url, body) { + async intercept(url, body) { return new Promise(async (resolve, reject) => { try { let newHtml = body; @@ -59,7 +59,7 @@ class PolyfillsResource extends ResourceInterface { `); } - resolve(newHtml); + resolve({ body: newHtml }); } catch (e) { reject(e); } diff --git a/packages/plugin-polyfills/test/cases/dsd/dsd.spec.js b/packages/plugin-polyfills/test/cases/dsd/dsd.spec.js index dba03f1b5..ac5be9443 100644 --- a/packages/plugin-polyfills/test/cases/dsd/dsd.spec.js +++ b/packages/plugin-polyfills/test/cases/dsd/dsd.spec.js @@ -74,7 +74,7 @@ describe('Build Greenwood With: ', function() { it('should have the expected DSD polyfill content in the polyfill