From ee4d0a4f7ec56046b3c67d8e9aea182660f25978 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 28 Jun 2023 21:46:26 -0400 Subject: [PATCH 01/35] Enhancement/issue 1088 refactor workers out of SSR builds (#1110) * production SSR workers refactor WIP * initial draft refactoring for no Workers as part of serving SSR builds * decouple SSR module execution from Workers implementation * enable pre-compiled HTML for templates during SSR * ammed static router spec for execute-route-module * get SSR execution module from config * refactor executeRouteModule signature and fix all specs * update lit renderer per execute module refactoring * pre-bundle SSR entry points * refactor entry file to use runtime import.meta.url * use placholder for SSR page entry point path and replace at write with rollup * expand rollup and lit circular reference TODO comment * clean up console logs and track TODOs * update Renderer plugin docs --- packages/cli/src/commands/build.js | 2 +- packages/cli/src/config/rollup.config.js | 59 +++++++- packages/cli/src/lib/execute-route-module.js | 42 ++++++ packages/cli/src/lib/ssr-route-worker.js | 42 +----- packages/cli/src/lib/templating-utils.js | 2 + packages/cli/src/lifecycles/bundle.js | 130 +++++++----------- packages/cli/src/lifecycles/graph.js | 16 ++- packages/cli/src/lifecycles/prerender.js | 5 +- packages/cli/src/lifecycles/serve.js | 2 +- .../renderer/plugin-renderer-default.js | 2 +- .../plugins/resource/plugin-standard-html.js | 7 +- .../src/pages/artists.js | 2 +- .../cases/develop.ssr/src/pages/artists.js | 4 +- .../serve.config.static-router.spec.js | 61 +------- .../src/pages/artists.js | 2 +- .../serve.default.ssr.spec.js | 6 - .../serve.default.ssr/src/pages/artists.js | 4 +- ...-worker-lit.js => execute-route-module.js} | 20 +-- packages/plugin-renderer-lit/src/index.js | 2 +- .../cases/serve.default/serve.default.spec.js | 7 +- .../cases/serve.default/src/pages/artists.js | 4 +- www/pages/plugins/renderer.md | 19 ++- 22 files changed, 202 insertions(+), 238 deletions(-) create mode 100644 packages/cli/src/lib/execute-route-module.js rename packages/plugin-renderer-lit/src/{ssr-route-worker-lit.js => execute-route-module.js} (68%) diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js index 1d4502195..6ea8d15e9 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.js @@ -98,7 +98,7 @@ const runProductionBuild = async (compilation) => { return Promise.resolve(server); })); - if (prerenderPlugin.workerUrl) { + if (prerenderPlugin.executeModuleUrl) { await trackResourcesForRoutes(compilation); await preRenderCompilationWorker(compilation, prerenderPlugin); } else { diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index f3d0ebd4f..10e321a35 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -130,6 +130,31 @@ function greenwoodSyncPageResourceBundlesPlugin(compilation) { }; } +// TODO could we use this instead? +// https://github.com/rollup/rollup/blob/v2.79.1/docs/05-plugin-development.md#resolveimportmeta +// https://github.com/ProjectEvergreen/greenwood/issues/1087 +function greenwoodPatchSsrPagesEntryPointRuntimeImport() { + return { + name: 'greenwood-patch-ssr-pages-entry-point-runtime-import', + generateBundle(options, bundle) { + Object.keys(bundle).forEach((key) => { + if (key.startsWith('__')) { + console.log('this is a generated entry point', bundle[key]); + // ___GWD_ENTRY_FILE_URL=${filename}___ + const needle = bundle[key].code.match(/___GWD_ENTRY_FILE_URL=(.*.)___/); + if (needle) { + const entryPathMatch = needle[1]; + + bundle[key].code = bundle[key].code.replace(/'___GWD_ENTRY_FILE_URL=(.*.)___'/, `new URL('./_${entryPathMatch}', import.meta.url)`); + } else { + console.warn(`Could not find entry path match for bundle => ${ley}`); + } + } + }); + } + }; +} + const getRollupConfigForScriptResources = async (compilation) => { const { outputDir } = compilation.context; const input = [...compilation.resources.values()] @@ -193,7 +218,7 @@ const getRollupConfigForApis = async (compilation) => { .map(api => normalizePathnameForWindows(new URL(`.${api.path}`, userWorkspace))); // TODO should routes and APIs have chunks? - // https://github.com/ProjectEvergreen/greenwood/issues/1008 + // https://github.com/ProjectEvergreen/greenwood/issues/1118 return [{ input, output: { @@ -214,7 +239,7 @@ const getRollupConfigForSsr = async (compilation, input) => { const { outputDir } = compilation.context; // TODO should routes and APIs have chunks? - // https://github.com/ProjectEvergreen/greenwood/issues/1008 + // https://github.com/ProjectEvergreen/greenwood/issues/1118 return [{ input, output: { @@ -224,10 +249,34 @@ const getRollupConfigForSsr = async (compilation, input) => { }, plugins: [ greenwoodJsonLoader(), - nodeResolve(), + // TODO let this through for lit to enable nodeResolve({ preferBuiltins: true }) + // https://github.com/lit/lit/issues/449 + // https://github.com/ProjectEvergreen/greenwood/issues/1118 + nodeResolve({ + preferBuiltins: true + }), commonjs(), - importMetaAssets() - ] + importMetaAssets(), + greenwoodPatchSsrPagesEntryPointRuntimeImport() // TODO a little hacky but works for now + ], + onwarn: (errorObj) => { + const { code, message } = errorObj; + + switch (code) { + + case 'CIRCULAR_DEPENDENCY': + // TODO let this through for lit by suppressing it + // Error: the string "Circular dependency: ../../../../../node_modules/@lit-labs/ssr/lib/render-lit-html.js -> + // ../../../../../node_modules/@lit-labs/ssr/lib/lit-element-renderer.js -> ../../../../../node_modules/@lit-labs/ssr/lib/render-lit-html.js\n" was thrown, throw an Error :) + // https://github.com/lit/lit/issues/449 + // https://github.com/ProjectEvergreen/greenwood/issues/1118 + break; + default: + // otherwise, log all warnings from rollup + console.debug(message); + + } + } }]; }; diff --git a/packages/cli/src/lib/execute-route-module.js b/packages/cli/src/lib/execute-route-module.js new file mode 100644 index 000000000..5b36631fe --- /dev/null +++ b/packages/cli/src/lib/execute-route-module.js @@ -0,0 +1,42 @@ +import { renderToString, renderFromHTML } from 'wc-compiler'; + +async function executeRouteModule({ moduleUrl, compilation, page = {}, prerender = false, htmlContents = null, scripts = [] }) { + const data = { + template: null, + body: null, + frontmatter: null, + html: null + }; + + if (prerender) { + const scriptURLs = scripts.map(scriptFile => new URL(scriptFile)); + const { html } = await renderFromHTML(htmlContents, scriptURLs); + + data.html = html; + } else { + const module = await import(moduleUrl).then(module => module); + const { getTemplate = null, getBody = null, getFrontmatter = null } = module; + + if (module.default) { + const { html } = await renderToString(new URL(moduleUrl), false); + + data.body = html; + } else { + if (getBody) { + data.body = await getBody(compilation, page); + } + } + + if (getTemplate) { + data.template = await getTemplate(compilation, page); + } + + if (getFrontmatter) { + data.frontmatter = await getFrontmatter(compilation, page); + } + } + + return data; +} + +export { executeRouteModule }; \ 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 490624abe..12eb71d88 100644 --- a/packages/cli/src/lib/ssr-route-worker.js +++ b/packages/cli/src/lib/ssr-route-worker.js @@ -1,47 +1,13 @@ // https://github.com/nodejs/modules/issues/307#issuecomment-858729422 import { parentPort } from 'worker_threads'; -import { renderToString, renderFromHTML } from 'wc-compiler'; -async function executeRouteModule({ moduleUrl, compilation, route, label, id, prerender, htmlContents, scripts }) { - const parsedCompilation = JSON.parse(compilation); - const data = { - template: null, - body: null, - frontmatter: null, - html: null - }; - - if (prerender) { - const scriptURLs = JSON.parse(scripts).map(scriptFile => new URL(scriptFile)); - const { html } = await renderFromHTML(htmlContents, scriptURLs); - - data.html = html; - } else { - const module = await import(moduleUrl).then(module => module); - const { getTemplate = null, getBody = null, getFrontmatter = null } = module; - - if (module.default) { - const { html } = await renderToString(new URL(moduleUrl), false); - - data.body = html; - } else { - if (getBody) { - data.body = await getBody(parsedCompilation, route); - } - } - - if (getTemplate) { - data.template = await getTemplate(parsedCompilation, route); - } - - if (getFrontmatter) { - data.frontmatter = await getFrontmatter(parsedCompilation, route, label, id); - } - } +async function executeModule({ executeModuleUrl, moduleUrl, compilation, page, prerender = false, htmlContents = null, scripts = '[]' }) { + const { executeRouteModule } = await import(executeModuleUrl); + const data = await executeRouteModule({ moduleUrl, compilation: JSON.parse(compilation), page: JSON.parse(page), prerender, htmlContents, scripts: JSON.parse(scripts) }); parentPort.postMessage(data); } parentPort.on('message', async (task) => { - await executeRouteModule(task); + await executeModule(task); }); \ No newline at end of file diff --git a/packages/cli/src/lib/templating-utils.js b/packages/cli/src/lib/templating-utils.js index 39486a53e..9adbed1d1 100644 --- a/packages/cli/src/lib/templating-utils.js +++ b/packages/cli/src/lib/templating-utils.js @@ -177,6 +177,8 @@ async function getAppTemplate(pageTemplateContents, context, customImports = [], } async function getUserScripts (contents, context) { + // TODO get rid of lit polyfills in core + // https://github.com/ProjectEvergreen/greenwood/issues/728 // https://lit.dev/docs/tools/requirements/#polyfills if (process.env.__GWD_COMMAND__ === 'build') { // eslint-disable-line no-underscore-dangle const userPackageJson = await getPackageJson(context); diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index 974619223..b148402fa 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -1,6 +1,7 @@ /* eslint-disable max-depth, max-len */ import fs from 'fs/promises'; import { getRollupConfigForApis, getRollupConfigForScriptResources, getRollupConfigForSsr } from '../config/rollup.config.js'; +import { getAppTemplate, getPageTemplate, getUserScripts } from '../lib/templating-utils.js'; import { hashString } from '../lib/hashing-utils.js'; import { checkResourceExists, mergeResponse, normalizePathnameForWindows } from '../lib/resource-utils.js'; import path from 'path'; @@ -174,7 +175,6 @@ async function bundleApiRoutes(compilation) { async function bundleSsrPages(compilation) { // https://rollupjs.org/guide/en/#differences-to-the-javascript-api - const { outputDir, pagesDir } = compilation.context; // TODO context plugins for SSR ? // https://github.com/ProjectEvergreen/greenwood/issues/1008 // const contextPlugins = compilation.config.plugins.filter((plugin) => { @@ -182,100 +182,61 @@ async function bundleSsrPages(compilation) { // }).map((plugin) => { // return plugin.provider(compilation); // }); - + const hasSSRPages = compilation.graph.filter(page => page.isSSR).length > 0; const input = []; - if (!compilation.config.prerender) { + if (!compilation.config.prerender && hasSSRPages) { + const htmlOptimizer = compilation.config.plugins.find(plugin => plugin.name === 'plugin-standard-html').provider(compilation); + const { executeModuleUrl } = compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider(); + const { executeRouteModule } = await import(executeModuleUrl); + const { pagesDir, scratchDir } = compilation.context; + for (const page of compilation.graph) { if (page.isSSR && !page.data.static) { - const { filename, path: pagePath } = page; - const scratchUrl = new URL(`./${filename}`, outputDir); - - // better way to write out inline code like this? - await fs.writeFile(scratchUrl, ` - import { Worker } from 'worker_threads'; - import { getAppTemplate, getPageTemplate, getUserScripts } from '@greenwood/cli/src/lib/templating-utils.js'; - - export async function handler(request, compilation) { - const routeModuleLocationUrl = new URL('./_${filename}', '${outputDir}'); - const routeWorkerUrl = '${compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().workerUrl}'; - const htmlOptimizer = compilation.config.plugins.find(plugin => plugin.name === 'plugin-standard-html').provider(compilation); - let body = ''; - let html = ''; - let frontmatter; - let template; - let templateType = 'page'; - let title = ''; - let imports = []; - - await new Promise((resolve, reject) => { - const worker = new Worker(new URL(routeWorkerUrl)); - - worker.on('message', (result) => { - if (result.body) { - body = result.body; - } - - if (result.template) { - template = result.template; - } - - if (result.frontmatter) { - frontmatter = result.frontmatter; - - if (frontmatter.title) { - title = frontmatter.title; - } - - if (frontmatter.template) { - templateType = frontmatter.template; - } - - if (frontmatter.imports) { - imports = imports.concat(frontmatter.imports); - } - } - - resolve(); - }); - - worker.on('error', reject); - worker.on('exit', (code) => { - if (code !== 0) { - reject(new Error(\`Worker stopped with exit code \${code}\`)); - } - }); - - worker.postMessage({ - moduleUrl: routeModuleLocationUrl.href, - compilation: \`${JSON.stringify(compilation)}\`, - route: '${pagePath}' - }); - }); - - html = template ? template : await getPageTemplate('', compilation.context, templateType, []); - html = await getAppTemplate(html, compilation.context, imports, [], false, title); - html = await getUserScripts(html, compilation.context); - html = html.replace(\/\(.*)<\\/content-outlet>\/s, body); - html = await (await htmlOptimizer.optimize(new URL(request.url), new Response(html))).text(); - - return new Response(html); + const { filename, imports, route, template, title } = page; + const entryFileUrl = new URL(`./_${filename}`, scratchDir); + const moduleUrl = new URL(`./${filename}`, pagesDir); + // TODO getTemplate has to be static (for now?) + // https://github.com/ProjectEvergreen/greenwood/issues/955 + const data = await executeRouteModule({ moduleUrl, compilation, page, prerender: false, htmlContents: null, scripts: [] }); + let staticHtml = ''; + + staticHtml = data.template ? data.template : await getPageTemplate(staticHtml, compilation.context, template, []); + staticHtml = await getAppTemplate(staticHtml, compilation.context, imports, [], false, title); + staticHtml = await getUserScripts(staticHtml, compilation.context); + staticHtml = await (await htmlOptimizer.optimize(new URL(`http://localhost:8080${route}`), new Response(staticHtml))).text(); + + // better way to write out this inline code? + await fs.writeFile(entryFileUrl, ` + import { executeRouteModule } from '${normalizePathnameForWindows(executeModuleUrl)}'; + + export async function handler(request) { + const compilation = JSON.parse('${JSON.stringify(compilation)}'); + const page = JSON.parse('${JSON.stringify(page)}'); + const moduleUrl = '___GWD_ENTRY_FILE_URL=${filename}___'; + const data = await executeRouteModule({ moduleUrl, compilation, page }); + let staticHtml = \`${staticHtml}\`; + + // console.log({ page }) + // console.log({ staticHtml }) + // console.log({ data }); + + if (data.body) { + staticHtml = staticHtml.replace(\/\(.*)<\\/content-outlet>\/s, data.body); + } + + return new Response(staticHtml); } `); - input.push(normalizePathnameForWindows(new URL(`./${filename}`, pagesDir))); + input.push(normalizePathnameForWindows(moduleUrl)); + input.push(normalizePathnameForWindows(entryFileUrl)); } } const [rollupConfig] = await getRollupConfigForSsr(compilation, input); if (rollupConfig.input.length > 0) { - const { userTemplatesDir, outputDir } = compilation.context; - - if (await checkResourceExists(userTemplatesDir)) { - await fs.cp(userTemplatesDir, new URL('./_templates/', outputDir), { recursive: true }); - } - const bundle = await rollup(rollupConfig); await bundle.write(rollupConfig.output); } @@ -309,13 +270,14 @@ const bundleCompilation = async (compilation) => { await Promise.all([ await bundleApiRoutes(compilation), - await bundleSsrPages(compilation), await bundleScriptResources(compilation), await bundleStyleResources(compilation, optimizeResourcePlugins) ]); - console.info('optimizing static pages....'); + // bundleSsrPages depends on bundleScriptResources having run first + await bundleSsrPages(compilation); + console.info('optimizing static pages....'); await optimizeStaticPages(compilation, optimizeResourcePlugins); await cleanUpResources(compilation); await emitResources(compilation); diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js index 8c66ab3ae..486adb6d1 100644 --- a/packages/cli/src/lifecycles/graph.js +++ b/packages/cli/src/lifecycles/graph.js @@ -116,13 +116,13 @@ const generateGraph = async (compilation) => { } /* ---------End Menu Query-------------------- */ } else if (isDynamic) { - const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation).workerUrl; + const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation).executeModuleUrl; let ssrFrontmatter; filePath = route; await new Promise((resolve, reject) => { - const worker = new Worker(routeWorkerUrl); + const worker = new Worker(new URL('../lib/ssr-route-worker.js', import.meta.url)); worker.on('message', async (result) => { if (result.frontmatter) { @@ -139,9 +139,19 @@ const generateGraph = async (compilation) => { }); worker.postMessage({ + executeModuleUrl: routeWorkerUrl.href, moduleUrl: filenameUrl.href, compilation: JSON.stringify(compilation), - route + // TODO need to get as many of these params as possible + // or ignore completely? + page: JSON.stringify({ + route, + id, + label: id.split('-') + .map((idPart) => { + return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; + }).join(' ') + }) }); }); diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 48f8e8fdd..5f4caf964 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -55,7 +55,7 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { console.info('pages to generate', `\n ${pages.map(page => page.route).join('\n ')}`); - const pool = new WorkerPool(os.cpus().length, workerPrerender.workerUrl); + const pool = new WorkerPool(os.cpus().length, new URL('../lib/ssr-route-worker.js', import.meta.url)); for (const page of pages) { const { route, outputPath, resources } = page; @@ -76,9 +76,10 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { body = await new Promise((resolve, reject) => { pool.runTask({ + executeModuleUrl: workerPrerender.executeModuleUrl.href, modulePath: null, compilation: JSON.stringify(compilation), - route, + page: JSON.stringify(page), prerender: true, htmlContents: body, scripts: JSON.stringify(scripts) diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index b11bcb21c..8c3fc08f5 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -300,7 +300,7 @@ async function getHybridServer(compilation) { }); if (!config.prerender && matchingRoute.isSSR && !matchingRoute.data.static) { - const { handler } = await import(new URL(`./${matchingRoute.filename}`, outputDir)); + const { handler } = await import(new URL(`./__${matchingRoute.filename}`, outputDir)); // TODO passing compilation this way too hacky? // https://github.com/ProjectEvergreen/greenwood/issues/1008 const response = await handler(request, compilation); diff --git a/packages/cli/src/plugins/renderer/plugin-renderer-default.js b/packages/cli/src/plugins/renderer/plugin-renderer-default.js index 16203beb9..534384f83 100644 --- a/packages/cli/src/plugins/renderer/plugin-renderer-default.js +++ b/packages/cli/src/plugins/renderer/plugin-renderer-default.js @@ -3,7 +3,7 @@ const greenwoodPluginRendererDefault = { name: 'plugin-renderer-default', provider: () => { return { - workerUrl: new URL('../../lib/ssr-route-worker.js', import.meta.url) + executeModuleUrl: new URL('../../lib/execute-route-module.js', import.meta.url) }; } }; diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index fd37aafd7..4b91d69bc 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -105,10 +105,10 @@ class StandardHtmlResource extends ResourceInterface { if (matchingRoute.isSSR) { const routeModuleLocationUrl = new URL(`./${matchingRoute.filename}`, pagesDir); - const routeWorkerUrl = this.compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().workerUrl; + const routeWorkerUrl = this.compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().executeModuleUrl; await new Promise((resolve, reject) => { - const worker = new Worker(routeWorkerUrl); + const worker = new Worker(new URL('../../lib/ssr-route-worker.js', import.meta.url)); worker.on('message', (result) => { if (result.template) { @@ -143,9 +143,10 @@ class StandardHtmlResource extends ResourceInterface { }); worker.postMessage({ + executeModuleUrl: routeWorkerUrl.href, moduleUrl: routeModuleLocationUrl.href, compilation: JSON.stringify(this.compilation), - route: matchingRoute.path + page: JSON.stringify(matchingRoute) }); }); } diff --git a/packages/cli/test/cases/build.default.ssr-static-export/src/pages/artists.js b/packages/cli/test/cases/build.default.ssr-static-export/src/pages/artists.js index 740a563a1..e0d3773e5 100644 --- a/packages/cli/test/cases/build.default.ssr-static-export/src/pages/artists.js +++ b/packages/cli/test/cases/build.default.ssr-static-export/src/pages/artists.js @@ -1,4 +1,4 @@ -async function getTemplate(compilation, route) { +async function getTemplate(compilation, { route }) { return ` diff --git a/packages/cli/test/cases/develop.ssr/src/pages/artists.js b/packages/cli/test/cases/develop.ssr/src/pages/artists.js index ba2af1e62..b035a91eb 100644 --- a/packages/cli/test/cases/develop.ssr/src/pages/artists.js +++ b/packages/cli/test/cases/develop.ssr/src/pages/artists.js @@ -1,4 +1,4 @@ -async function getTemplate(compilation, route) { +async function getTemplate(compilation, { route }) { return ` @@ -67,7 +67,7 @@ async function getBody(compilation) { `; } -async function getFrontmatter(compilation, route) { +async function getFrontmatter(compilation, { route }) { return { menu: 'navigation', index: 7, diff --git a/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js b/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js index 369ad63f6..501b8129a 100644 --- a/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js +++ b/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js @@ -22,10 +22,8 @@ * index.md */ import chai from 'chai'; -import fs from 'fs/promises'; import path from 'path'; import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; -import { normalizePathnameForWindows } from '../../../src/lib/resource-utils.js'; import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; @@ -48,79 +46,22 @@ describe('Serve Greenwood With: ', function() { }); describe(LABEL, function() { - const workaroundFiles = [ - 'hashing-utils', - 'node-modules-utils', - 'resource-utils', - 'templating-utils' - ]; before(async function() { const greenwoodRouterLibs = await getDependencyFiles( `${process.cwd()}/packages/cli/src/lib/router.js`, `${outputPath}/node_modules/@greenwood/cli/src/lib` ); - /* - * there is an odd issue seemingly due to needed lib/router.js tha causes tests to think files are CommonJS - * ``` - * file:///Users/owenbuckley/Workspace/project-evergreen/repos/greenwood/packages/cli/test/cases/serve.config.static-router/public/artists.js:3 - * import { getAppTemplate, getPageTemplate, getUserScripts } from '@greenwood/cli/src/lib/templating-utils.js'; - * ^^^^^^^^^^^^^^ - * SyntaxError: Named export 'getAppTemplate' not found. The requested module '@greenwood/cli/src/lib/templating-utils.js' - * is a CommonJS module, which may not support all module.exports as named exports. - * CommonJS modules can always be imported via the default export, for example using: - * import pkg from '@greenwood/cli/src/lib/templating-utils.js'; - * const { getAppTemplate, getPageTemplate, getUserScripts } = pkg; - * ``` - * - * however no other tests have this issue. so as terrible hack we need to - * - copy all lib files - * - rename them to end in .mjs - * - update references to these files in other imports - * - * (unfortunately, trying to just add a package.json with type="module" did not seem to work :/) - */ - const greenwoodTemplatingLibs = await getDependencyFiles( - `${process.cwd()}/packages/cli/src/lib/*`, - `${outputPath}/node_modules/@greenwood/cli/src/lib` - ); - const greenwoodTemplates = await getDependencyFiles( - `${process.cwd()}/packages/cli/src/templates/*`, - `${outputPath}/node_modules/@greenwood/cli/src/templates` - ); await runner.setup(outputPath, [ ...getSetupFiles(outputPath), - ...greenwoodRouterLibs, - ...greenwoodTemplatingLibs, - ...greenwoodTemplates + ...greenwoodRouterLibs ]); - for (const f of workaroundFiles) { - const pathname = normalizePathnameForWindows(new URL(`./node_modules/@greenwood/cli/src/lib/${f}.js`, import.meta.url)); - let contents = await fs.readFile(pathname, 'utf-8'); - - workaroundFiles.forEach((wf) => { - contents = contents.replace(`${wf}.js`, `${wf}.mjs`); - }); - - await fs.writeFile(pathname.replace('.js', '.mjs'), contents); - } - await runner.runCommand(cliPath, 'build'); return new Promise(async (resolve) => { setTimeout(async () => { - // template out artists.js to use .mjs too - const pathname = normalizePathnameForWindows(new URL('./public/artists.js', import.meta.url)); - let ssrPageContents = await fs.readFile(pathname, 'utf-8'); - - for (const f of workaroundFiles) { - ssrPageContents = ssrPageContents.replace(`${f}.js`, `${f}.mjs`); - } - - await fs.writeFile(pathname, ssrPageContents); - resolve(); }, 10000); diff --git a/packages/cli/test/cases/serve.default.ssr-static-export/src/pages/artists.js b/packages/cli/test/cases/serve.default.ssr-static-export/src/pages/artists.js index 740a563a1..e0d3773e5 100644 --- a/packages/cli/test/cases/serve.default.ssr-static-export/src/pages/artists.js +++ b/packages/cli/test/cases/serve.default.ssr-static-export/src/pages/artists.js @@ -1,4 +1,4 @@ -async function getTemplate(compilation, route) { +async function getTemplate(compilation, { route }) { return ` diff --git a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js index da4f85f14..2eb743a67 100644 --- a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js +++ b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js @@ -139,12 +139,6 @@ describe('Serve Greenwood With: ', function() { expect(scriptFiles.length).to.equal(2); }); - - it('should have the expected _templates/ output directory for the app', async function() { - const templateFiles = await glob.promise(path.join(this.context.publicDir, '_templates/*')); - - expect(templateFiles.length).to.equal(1); - }); }); describe('Serve command with HTML route response for artists page using "get" functions', function() { diff --git a/packages/cli/test/cases/serve.default.ssr/src/pages/artists.js b/packages/cli/test/cases/serve.default.ssr/src/pages/artists.js index 46a4b4c4d..bc5c1d2a9 100644 --- a/packages/cli/test/cases/serve.default.ssr/src/pages/artists.js +++ b/packages/cli/test/cases/serve.default.ssr/src/pages/artists.js @@ -1,4 +1,4 @@ -async function getTemplate(compilation, route) { +async function getTemplate(compilation, { route }) { return ` @@ -68,7 +68,7 @@ async function getBody(compilation) { `; } -async function getFrontmatter(compilation, route) { +async function getFrontmatter(compilation, { route }) { return { menu: 'navigation', title: route, diff --git a/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js b/packages/plugin-renderer-lit/src/execute-route-module.js similarity index 68% rename from packages/plugin-renderer-lit/src/ssr-route-worker-lit.js rename to packages/plugin-renderer-lit/src/execute-route-module.js index 4d96c71fa..5e73f7505 100644 --- a/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js +++ b/packages/plugin-renderer-lit/src/execute-route-module.js @@ -4,7 +4,6 @@ import { Buffer } from 'buffer'; import { html } from 'lit'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { Readable } from 'stream'; -import { parentPort } from 'worker_threads'; async function streamToString (stream) { const chunks = []; @@ -20,9 +19,7 @@ async function getTemplateResultString(template) { return await streamToString(Readable.from(render(template))); } -async function executeRouteModule({ moduleUrl, compilation, route, label, id, prerender, htmlContents, scripts }) { - const parsedCompilation = JSON.parse(compilation); - const parsedScripts = scripts ? JSON.parse(scripts) : []; +async function executeRouteModule({ moduleUrl, compilation, page, prerender, htmlContents, scripts }) { const data = { template: null, body: null, @@ -30,10 +27,9 @@ async function executeRouteModule({ moduleUrl, compilation, route, label, id, pr html: null }; - console.debug({ moduleUrl }); // prerender static content if (prerender) { - for (const script of parsedScripts) { + for (const script of scripts) { await import(script); } @@ -52,25 +48,23 @@ async function executeRouteModule({ moduleUrl, compilation, route, label, id, pr data.body = await getTemplateResultString(templateResult); } else if (getBody) { - const templateResult = await getBody(parsedCompilation, route); + const templateResult = await getBody(compilation, page); data.body = await getTemplateResultString(templateResult); } if (getTemplate) { - const templateResult = await getTemplate(parsedCompilation, route); + const templateResult = await getTemplate(compilation, page); data.template = await getTemplateResultString(templateResult); } if (getFrontmatter) { - data.frontmatter = await getFrontmatter(parsedCompilation, route, label, id); + data.frontmatter = await getFrontmatter(compilation, page); } } - parentPort.postMessage(data); + return data; } -parentPort.on('message', async (task) => { - await executeRouteModule(task); -}); \ No newline at end of file +export { executeRouteModule }; \ No newline at end of file diff --git a/packages/plugin-renderer-lit/src/index.js b/packages/plugin-renderer-lit/src/index.js index 4c06c0692..e4e190be0 100755 --- a/packages/plugin-renderer-lit/src/index.js +++ b/packages/plugin-renderer-lit/src/index.js @@ -4,7 +4,7 @@ const greenwoodPluginRendererLit = (options = {}) => { name: 'plugin-renderer-lit', provider: () => { return { - workerUrl: new URL('./ssr-route-worker-lit.js', import.meta.url), + executeModuleUrl: new URL('./execute-route-module.js', import.meta.url), prerender: options.prerender }; } diff --git a/packages/plugin-renderer-lit/test/cases/serve.default/serve.default.spec.js b/packages/plugin-renderer-lit/test/cases/serve.default/serve.default.spec.js index e317401de..9a975f336 100644 --- a/packages/plugin-renderer-lit/test/cases/serve.default/serve.default.spec.js +++ b/packages/plugin-renderer-lit/test/cases/serve.default/serve.default.spec.js @@ -203,10 +203,13 @@ describe('Serve Greenwood With: ', function() { expect(styles.length).to.equal(1); }); - it('should have no + From 3432617916d9162c576e8d0d85651b0d64c173ed Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 13 Oct 2023 20:17:36 -0400 Subject: [PATCH 19/35] bug/issue 1151 custom imports not working for API routes and SSR pages (#1152) * custom imports for API routes and SSR pages and custom css import test cases * add test cases for import json plugin * all specs passing * refactor rollup id cleaning * import meta url refactoring * normalize bundled import meta URL paths for Windows * full bundling support for custom imports * update final TODO comments * leverage custom imports that can serve for meta import chunk vs asset bundling * refactor custom import detection for found assetUrls * add test case for bundling images with new URL * document new URL and import.meta.url pattern * clarify docs on new URL usage patterns and general content around assets * fix windows specs * callout support for isomorphic asset bundling in docs --------- Co-authored-by: Owen Buckley --- .eslintignore | 3 +- packages/cli/package.json | 1 - packages/cli/src/config/rollup.config.js | 181 ++++++++++++++++-- .../serve.default.ssr.spec.js | 56 ++++++ .../serve.default.ssr/src/components/card.js | 2 + .../serve.default.ssr/src/images/logo.svg | 45 +++++ .../cases/exp-serve.ssr/exp-serve.ssr.spec.js | 178 +++++++++++++++++ .../cases/exp-serve.ssr/greenwood.config.js | 7 + .../test/cases/exp-serve.ssr/package.json | 4 + .../cases/exp-serve.ssr/src/api/fragment.js | 28 +++ .../exp-serve.ssr/src/components/card.css | 44 +++++ .../exp-serve.ssr/src/components/card.js | 31 +++ .../cases/exp-serve.ssr/src/pages/products.js | 31 +++ .../exp-serve.ssr/src/services/products.js | 11 ++ .../cases/exp-serve.ssr/src/styles/some.css | 3 + .../cases/exp-serve.ssr/exp-serve.ssr.spec.js | 161 ++++++++++++++++ .../cases/exp-serve.ssr/greenwood.config.js | 7 + .../test/cases/exp-serve.ssr/package.json | 4 + .../cases/exp-serve.ssr/src/api/fragment.js | 27 +++ .../exp-serve.ssr/src/components/card.js | 26 +++ .../exp-serve.ssr/src/data/products.json | 7 + .../cases/exp-serve.ssr/src/pages/products.js | 26 +++ .../cases/exp-serve.ssr/exp-serve.ssr.spec.js | 118 ++++++++++++ .../cases/exp-serve.ssr/greenwood.config.js | 7 + .../test/cases/exp-serve.ssr/package.json | 4 + .../cases/exp-serve.ssr/src/api/fragment.js | 33 ++++ .../exp-serve.ssr/src/components/card/card.ts | 33 ++++ .../src/components/card/logo.png | Bin 0 -> 2171 bytes .../src/components/card/styles.ts | 7 + .../cases/exp-serve.ssr/src/pages/index.html | 16 ++ www/pages/docs/css-and-images.md | 52 +++-- yarn.lock | 13 +- 32 files changed, 1122 insertions(+), 44 deletions(-) create mode 100644 packages/cli/test/cases/serve.default.ssr/src/images/logo.svg create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/greenwood.config.js create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/package.json create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/src/api/fragment.js create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/src/components/card.css create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/src/components/card.js create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/src/pages/products.js create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/src/services/products.js create mode 100644 packages/plugin-import-css/test/cases/exp-serve.ssr/src/styles/some.css create mode 100644 packages/plugin-import-json/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js create mode 100644 packages/plugin-import-json/test/cases/exp-serve.ssr/greenwood.config.js create mode 100644 packages/plugin-import-json/test/cases/exp-serve.ssr/package.json create mode 100644 packages/plugin-import-json/test/cases/exp-serve.ssr/src/api/fragment.js create mode 100644 packages/plugin-import-json/test/cases/exp-serve.ssr/src/components/card.js create mode 100644 packages/plugin-import-json/test/cases/exp-serve.ssr/src/data/products.json create mode 100644 packages/plugin-import-json/test/cases/exp-serve.ssr/src/pages/products.js create mode 100644 packages/plugin-typescript/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js create mode 100644 packages/plugin-typescript/test/cases/exp-serve.ssr/greenwood.config.js create mode 100644 packages/plugin-typescript/test/cases/exp-serve.ssr/package.json create mode 100644 packages/plugin-typescript/test/cases/exp-serve.ssr/src/api/fragment.js create mode 100644 packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/card.ts create mode 100644 packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/logo.png create mode 100644 packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/styles.ts create mode 100644 packages/plugin-typescript/test/cases/exp-serve.ssr/src/pages/index.html diff --git a/.eslintignore b/.eslintignore index 7862cff23..8e786af06 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ **/node_modules/** !.eslintrc.cjs !.mocharc.js -packages/plugin-babel/test/cases/**/*main.js \ No newline at end of file +packages/plugin-babel/test/cases/**/*main.js +TODO.md \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index ec716fca9..7ac5e3ee9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,7 +34,6 @@ "@rollup/plugin-node-resolve": "^13.0.0", "@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-terser": "^0.1.0", - "@web/rollup-plugin-import-meta-assets": "^1.0.0", "acorn": "^8.0.1", "acorn-walk": "^8.0.0", "commander": "^2.20.0", diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index df59ef61f..4c7156860 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -1,20 +1,27 @@ -import fs from 'fs/promises'; +import fs from 'fs'; +import path from 'path'; import { checkResourceExists, normalizePathnameForWindows } from '../lib/resource-utils.js'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; -import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets'; +import * as walk from 'acorn-walk'; -// specifically to handle escodegen using require for package.json +// https://github.com/rollup/rollup/issues/2121 +function cleanRollupId(id) { + return id.replace('\x00', ''); +} + +// specifically to handle escodegen and other node modules +// using require for package.json or other json files // https://github.com/estools/escodegen/issues/455 function greenwoodJsonLoader() { return { name: 'greenwood-json-loader', async load(id) { - const extension = id.split('.').pop(); + const idUrl = new URL(`file://${cleanRollupId(id)}`); + const extension = idUrl.pathname.split('.').pop(); if (extension === 'json') { - const url = new URL(`file://${id}`); - const json = JSON.parse(await fs.readFile(url, 'utf-8')); + const json = JSON.parse(await fs.promises.readFile(idUrl, 'utf-8')); const contents = `export default ${JSON.stringify(json)}`; return contents; @@ -33,11 +40,11 @@ function greenwoodResourceLoader (compilation) { return { name: 'greenwood-resource-loader', async resolveId(id) { - const normalizedId = id.replace(/\?type=(.*)/, ''); + const normalizedId = cleanRollupId(id); // idUrl.pathname; const { projectDirectory, userWorkspace } = compilation.context; - if (id.startsWith('.') && !id.startsWith(projectDirectory.pathname)) { - const prefix = id.startsWith('..') ? './' : ''; + if (normalizedId.startsWith('.') && !normalizedId.startsWith(projectDirectory.pathname)) { + const prefix = normalizedId.startsWith('..') ? './' : ''; const userWorkspaceUrl = new URL(`${prefix}${normalizedId.replace(/\.\.\//g, '')}`, userWorkspace); if (await checkResourceExists(userWorkspaceUrl)) { @@ -46,11 +53,13 @@ function greenwoodResourceLoader (compilation) { } }, async load(id) { - const pathname = id.indexOf('?') >= 0 ? id.slice(0, id.indexOf('?')) : id; + const idUrl = new URL(`file://${cleanRollupId(id)}`); + const { pathname } = idUrl; const extension = pathname.split('.').pop(); - if (extension !== '' && extension !== 'js') { - const url = new URL(`file://${pathname}?type=${extension}`); + // filter first for any bare specifiers + if (await checkResourceExists(idUrl) && extension !== '' && extension !== 'js') { + const url = new URL(`${idUrl.href}?type=${extension}`); const request = new Request(url.href); let response = new Response(''); @@ -116,12 +125,12 @@ function greenwoodSyncPageResourceBundlesPlugin(compilation) { compilation.resources.set(resource.sourcePathURL.pathname, { ...compilation.resources.get(resource.sourcePathURL.pathname), optimizedFileName: fileName, - optimizedFileContents: await fs.readFile(outputPath, 'utf-8'), + optimizedFileContents: await fs.promises.readFile(outputPath, 'utf-8'), contents }); if (noop) { - await fs.writeFile(outputPath, contents); + await fs.promises.writeFile(outputPath, contents); } } } @@ -130,6 +139,138 @@ function greenwoodSyncPageResourceBundlesPlugin(compilation) { }; } +function getMetaImportPath(node) { + return node.arguments[0].value.split('/').join(path.sep); +} + +function isNewUrlImportMetaUrl(node) { + return ( + node.type === 'NewExpression' && + node.callee.type === 'Identifier' && + node.callee.name === 'URL' && + node.arguments.length === 2 && + node.arguments[0].type === 'Literal' && + typeof getMetaImportPath(node) === 'string' && + node.arguments[1].type === 'MemberExpression' && + node.arguments[1].object.type === 'MetaProperty' && + node.arguments[1].property.type === 'Identifier' && + node.arguments[1].property.name === 'url' + ); +} + +// adapted from, and with credit to @web/rollup-plugin-import-meta-assets +// https://modern-web.dev/docs/building/rollup-plugin-import-meta-assets/ +function greenwoodImportMetaUrl(compilation) { + + return { + name: 'greenwood-import-meta-url', + + async transform(code, id) { + const resourcePlugins = compilation.config.plugins.filter((plugin) => { + return plugin.type === 'resource'; + }).map((plugin) => { + return plugin.provider(compilation); + }); + const customResourcePlugins = compilation.config.plugins.filter((plugin) => { + return plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin; + }).map((plugin) => { + return plugin.provider(compilation); + }); + const idUrl = new URL(`file://${cleanRollupId(id)}`); + const { pathname } = idUrl; + const extension = pathname.split('.').pop(); + const urlWithType = new URL(`${idUrl.href}?type=${extension}`); + const request = new Request(urlWithType.href); + let canTransform = false; + let response = new Response(code); + + // handle any custom imports or pre-processing needed before passing to Rollup this.parse + if (await checkResourceExists(idUrl) && extension !== '' && extension !== 'json') { + for (const plugin of resourcePlugins) { + if (plugin.shouldServe && await plugin.shouldServe(urlWithType, request)) { + response = await plugin.serve(urlWithType, request); + canTransform = true; + } + } + + for (const plugin of resourcePlugins) { + if (plugin.shouldIntercept && await plugin.shouldIntercept(urlWithType, request, response.clone())) { + response = await plugin.intercept(urlWithType, request, response.clone()); + canTransform = true; + } + } + } + + if (!canTransform) { + return null; + } + + const ast = this.parse(await response.text()); + const assetUrls = []; + let modifiedCode = false; + + // aggregate all references of new URL + import.meta.url + walk.simple(ast, { + NewExpression(node) { + if (isNewUrlImportMetaUrl(node)) { + const absoluteScriptDir = path.dirname(id); + const relativeAssetPath = getMetaImportPath(node); + const absoluteAssetPath = path.resolve(absoluteScriptDir, relativeAssetPath); + const assetName = path.basename(absoluteAssetPath); + const assetExtension = assetName.split('.').pop(); + + assetUrls.push({ + url: new URL(`file://${absoluteAssetPath}?type=${assetExtension}`), + relativeAssetPath + }); + } + } + }); + + for (const assetUrl of assetUrls) { + const { url } = assetUrl; + const { pathname } = url; + const { relativeAssetPath } = assetUrl; + const assetName = path.basename(pathname); + const assetExtension = assetName.split('.').pop(); + const assetContents = await fs.promises.readFile(url, 'utf-8'); + const name = assetName.replace(`.${assetExtension}`, ''); + let bundleExtensions = ['js']; + + for (const plugin of customResourcePlugins) { + if (plugin.shouldServe && await plugin.shouldServe(url)) { + const response = await plugin.serve(url); + + if (response?.headers?.get('content-type') || ''.indexOf('text/javascript') >= 0) { + bundleExtensions = [...bundleExtensions, ...plugin.extensions]; + } + } + } + + const type = bundleExtensions.indexOf(assetExtension) >= 0 + ? 'chunk' + : 'asset'; + const emitConfig = type === 'chunk' + ? { type, id: normalizePathnameForWindows(url), name } + : { type, name: assetName, source: assetContents }; + const ref = this.emitFile(emitConfig); + // handle Windows style paths + const normalizedRelativeAssetPath = relativeAssetPath.replace(/\\/g, '/'); + const importRef = `import.meta.ROLLUP_FILE_URL_${ref}`; + + modifiedCode = code + .replace(`'${normalizedRelativeAssetPath}'`, importRef) + .replace(`"${normalizedRelativeAssetPath}"`, importRef); + } + + return { + code: modifiedCode ? modifiedCode : code, + map: null + }; + } + }; +} + // TODO could we use this instead? // https://github.com/rollup/rollup/blob/v2.79.1/docs/05-plugin-development.md#resolveimportmeta // https://github.com/ProjectEvergreen/greenwood/issues/1087 @@ -177,6 +318,7 @@ const getRollupConfigForScriptResources = async (compilation) => { plugins: [ greenwoodResourceLoader(compilation), greenwoodSyncPageResourceBundlesPlugin(compilation), + greenwoodImportMetaUrl(compilation), ...customRollupPlugins ], context: 'window', @@ -216,6 +358,11 @@ const getRollupConfigForApis = async (compilation) => { const input = [...compilation.manifest.apis.values()] .map(api => normalizePathnameForWindows(new URL(`.${api.path}`, userWorkspace))); + // why is this needed? + await fs.promises.mkdir(new URL('./api/assets/', outputDir), { + recursive: true + }); + // TODO should routes and APIs have chunks? // https://github.com/ProjectEvergreen/greenwood/issues/1118 return [{ @@ -227,9 +374,10 @@ const getRollupConfigForApis = async (compilation) => { }, plugins: [ greenwoodJsonLoader(), + greenwoodResourceLoader(compilation), nodeResolve(), commonjs(), - importMetaAssets() + greenwoodImportMetaUrl(compilation) ] }]; }; @@ -248,6 +396,7 @@ const getRollupConfigForSsr = async (compilation, input) => { }, plugins: [ greenwoodJsonLoader(), + greenwoodResourceLoader(compilation), // TODO let this through for lit to enable nodeResolve({ preferBuiltins: true }) // https://github.com/lit/lit/issues/449 // https://github.com/ProjectEvergreen/greenwood/issues/1118 @@ -255,7 +404,7 @@ const getRollupConfigForSsr = async (compilation, input) => { preferBuiltins: true }), commonjs(), - importMetaAssets(), + greenwoodImportMetaUrl(compilation), greenwoodPatchSsrPagesEntryPointRuntimeImport() // TODO a little hacky but works for now ], onwarn: (errorObj) => { diff --git a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js index 6dde6c7fa..bf60ae24c 100644 --- a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js +++ b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js @@ -18,6 +18,8 @@ * components/ * card.js * counter.js + * images/ + * logo.svg * pages/ * about.md * artists.js @@ -300,6 +302,60 @@ describe('Serve Greenwood With: ', function() { }); }); + describe('Bundled image using new URL and import.meta.url', function() { + const bundledName = 'assets/logo-abb2e884.svg'; + let bundledImageResponse = {}; + let usersResponse = {}; + + before(async function() { + await new Promise((resolve, reject) => { + request.get(`${hostname}/${bundledName}`, (err, res, body) => { + if (err) { + reject(); + } + + bundledImageResponse = res; + bundledImageResponse.body = body; + + resolve(); + }); + }); + + await new Promise((resolve, reject) => { + request.get(`${hostname}/_users.js`, (err, res, body) => { + if (err) { + reject(); + } + + usersResponse = res; + usersResponse.body = body; + + resolve(); + }); + }); + }); + + it('should return a 200 status for the image', function(done) { + expect(bundledImageResponse.statusCode).to.equal(200); + done(); + }); + + it('should return the expected content-type for the image', function(done) { + expect(bundledImageResponse.headers['content-type']).to.equal('image/svg+xml'); + done(); + }); + + it('should return the expected body for the image', function(done) { + expect(bundledImageResponse.body.startsWith('= 0).to.equal(true); + done(); + }); + }); + describe('Serve command with 404 not found behavior', function() { let response = {}; diff --git a/packages/cli/test/cases/serve.default.ssr/src/components/card.js b/packages/cli/test/cases/serve.default.ssr/src/components/card.js index 21456531a..e6579a592 100644 --- a/packages/cli/test/cases/serve.default.ssr/src/components/card.js +++ b/packages/cli/test/cases/serve.default.ssr/src/components/card.js @@ -1,3 +1,4 @@ +const logo = new URL('../images/logo.svg', import.meta.url); const template = document.createElement('template'); template.innerHTML = ` @@ -23,6 +24,7 @@ template.innerHTML = `
+ logo My default title
diff --git a/packages/cli/test/cases/serve.default.ssr/src/images/logo.svg b/packages/cli/test/cases/serve.default.ssr/src/images/logo.svg new file mode 100644 index 000000000..75abb3ff2 --- /dev/null +++ b/packages/cli/test/cases/serve.default.ssr/src/images/logo.svg @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js b/packages/plugin-import-css/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js new file mode 100644 index 000000000..3a2b29f72 --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js @@ -0,0 +1,178 @@ +/* + * Use Case + * Run Greenwood with an API and SSR routes that import CSS. + * + * User Result + * Should generate a Greenwood build that correctly builds and bundles all assets. + * + * User Command + * greenwood build + * + * User Config + * { + * plugins: [ + * greenwoodPluginImportCss() + * ] + * } + * + * User Workspace + * src/ + * api/ + * fragment.js + * components/ + * card.js + * card.css + * pages/ + * products.js + * services/ + * products.js + * styles/ + * some.css + */ +import chai from 'chai'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import request from 'request'; +import { Runner } from 'gallinago'; +import { fileURLToPath } from 'url'; + +const expect = chai.expect; + +describe('Serve Greenwood With: ', function() { + const LABEL = 'A Server Rendered Application (SSR) with API Routes importing CSS'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://127.0.0.1:8080'; + let runner; + + before(async function() { + this.context = { + publicDir: path.join(outputPath, 'public'), + hostname + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + + before(async function() { + await runner.setup(outputPath, getSetupFiles(outputPath)); + await runner.runCommand(cliPath, 'build'); + + return new Promise(async (resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + + await runner.runCommand(cliPath, 'serve'); + }); + }); + + describe('Serve command with HTML route response for products page', function() { + let response = {}; + let productsPageDom; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}/products/`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + productsPageDom = new JSDOM(body); + + resolve(); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(response.body).to.not.be.undefined; + done(); + }); + + it('should have the expected import CSS in the page in the response body', function(done) { + const styleTag = productsPageDom.window.document.querySelectorAll('body > style'); + + expect(styleTag.length).to.equal(1); + expect(styleTag[0].textContent.replace(/ /g, '').replace(/\n/, '')).contain('h1{color:red;}'); + done(); + }); + + it('should make sure to have the expected CSS inlined into the page for each ', function(done) { + const cardComponents = productsPageDom.window.document.querySelectorAll('body app-card'); + + expect(cardComponents.length).to.equal(2); + Array.from(cardComponents).forEach((card) => { + expect(card.innerHTML).contain('display: flex;'); + }); + done(); + }); + }); + + describe('Serve command with API specific behaviors for an HTML ("fragment") API', function() { + let response = {}; + let fragmentsApiDom; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}/api/fragment`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + fragmentsApiDom = new JSDOM(body); + + resolve(); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return a custom status message', function(done) { + expect(response.statusMessage).to.equal('OK'); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); + + it('should make sure to have the expected CSS inlined into the page for each ', function(done) { + const cardComponents = fragmentsApiDom.window.document.querySelectorAll('body > app-card'); + + expect(cardComponents.length).to.equal(2); + Array.from(cardComponents).forEach((card) => { + expect(card.innerHTML).contain('display: flex;'); + }); + done(); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + runner.stopCommand(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/greenwood.config.js b/packages/plugin-import-css/test/cases/exp-serve.ssr/greenwood.config.js new file mode 100644 index 000000000..9c52e8e22 --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/greenwood.config.js @@ -0,0 +1,7 @@ +import { greenwoodPluginImportCss } from '../../../src/index.js'; + +export default { + plugins: [ + ...greenwoodPluginImportCss() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/package.json b/packages/plugin-import-css/test/cases/exp-serve.ssr/package.json new file mode 100644 index 000000000..96fd81923 --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-plugin-import-css-serve-ssr", + "type": "module" +} \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/src/api/fragment.js b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/api/fragment.js new file mode 100644 index 000000000..af4ced829 --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/api/fragment.js @@ -0,0 +1,28 @@ +import { renderFromHTML } from 'wc-compiler'; +import { getProducts } from '../services/products.js'; + +export async function handler() { + const products = await getProducts(); + const { html } = await renderFromHTML(` + ${ + products.map((product) => { + const { name, thumbnail } = product; + + return ` + + `; + }).join('') + } + `, [ + new URL('../components/card.js', import.meta.url) + ]); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/src/components/card.css b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/components/card.css new file mode 100644 index 000000000..db18c7c4a --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/components/card.css @@ -0,0 +1,44 @@ +div { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + border: 1px solid #818181; + width: fit-content; + border-radius: 10px; + padding: 2rem 1rem; + height: 680px; + justify-content: space-between; + background-color: #fff; + overflow-x: hidden; +} + +button { + background: var(--color-accent); + color: var(--color-white); + padding: 1rem 2rem; + border: 0; + font-size: 1rem; + border-radius: 5px; + cursor: pointer; +} + +img { + max-width: 500px; + min-width: 500px; + width: 100%; +} + +h3 { + font-size: 1.85rem; +} + +@media(max-width: 768px) { + img { + max-width: 300px; + min-width: 300px; + } + div { + height: 500px; + } +} \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/src/components/card.js b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/components/card.js new file mode 100644 index 000000000..faf3e725e --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/components/card.js @@ -0,0 +1,31 @@ +import styles from './card.css'; + +export default class Card extends HTMLElement { + + selectItem() { + alert(`selected item is => ${this.getAttribute('title')}!`); + } + + connectedCallback() { + if (!this.shadowRoot) { + const thumbnail = this.getAttribute('thumbnail'); + const title = this.getAttribute('title'); + const template = document.createElement('template'); + + template.innerHTML = ` + +
+

${title}

+ ${title} + +
+ `; + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + } +} + +customElements.define('app-card', Card); \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/src/pages/products.js b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/pages/products.js new file mode 100644 index 000000000..e5bf7b5bb --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/pages/products.js @@ -0,0 +1,31 @@ +import '../components/card.js'; +import { getProducts } from '../services/products.js'; +import styles from '../styles/some.css'; + +export default class ProductsPage extends HTMLElement { + async connectedCallback() { + const products = await getProducts(); + const html = products.map(product => { + const { name, thumbnail } = product; + + return ` + + + `; + }).join(''); + + this.innerHTML = ` +

SSR Page (w/ WCC)

+

List of Products: ${products.length}

+ +
+ ${html} +
+ `; + } +} \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/src/services/products.js b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/services/products.js new file mode 100644 index 000000000..96c999dac --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/services/products.js @@ -0,0 +1,11 @@ +async function getProducts() { + return [{ + name: 'iPhone 12', + thumbnail: 'iphone-12.png' + }, { + name: 'Samsung Galaxy', + thumbnail: 'samsung-galaxy.png' + }]; +} + +export { getProducts }; \ No newline at end of file diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/src/styles/some.css b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/styles/some.css new file mode 100644 index 000000000..9054080ff --- /dev/null +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/src/styles/some.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js b/packages/plugin-import-json/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js new file mode 100644 index 000000000..53035bf97 --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js @@ -0,0 +1,161 @@ +/* + * Use Case + * Run Greenwood with an API and SSR routes that import JSON. + * + * User Result + * Should generate a Greenwood build that correctly builds and bundles all assets. + * + * User Command + * greenwood build + * + * User Config + * { + * plugins: [ + * greenwoodPluginImportCss() + * ] + * } + * + * User Workspace + * src/ + * api/ + * fragment.js + * components/ + * card.js + * data/ + * products.json + * pages/ + * products.js + */ +import chai from 'chai'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import request from 'request'; +import { Runner } from 'gallinago'; +import { fileURLToPath } from 'url'; + +const expect = chai.expect; + +describe('Serve Greenwood With: ', function() { + const LABEL = 'A Server Rendered Application (SSR) with API Routes importing JSON'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://127.0.0.1:8080'; + let runner; + + before(async function() { + this.context = { + publicDir: path.join(outputPath, 'public'), + hostname + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + + before(async function() { + await runner.setup(outputPath, getSetupFiles(outputPath)); + await runner.runCommand(cliPath, 'build'); + + return new Promise(async (resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + + await runner.runCommand(cliPath, 'serve'); + }); + }); + + describe('Serve command with HTML route response for products page', function() { + let response = {}; + let productsPageDom; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}/products/`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + productsPageDom = new JSDOM(body); + + resolve(); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(response.body).to.not.be.undefined; + done(); + }); + + it('should make sure to have the expected number of components on the page', function(done) { + const cardComponents = productsPageDom.window.document.querySelectorAll('body app-card'); + + expect(cardComponents.length).to.equal(2); + done(); + }); + }); + + describe('Serve command with API specific behaviors for an HTML ("fragment") API', function() { + let response = {}; + let fragmentsApiDom; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}/api/fragment`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + fragmentsApiDom = new JSDOM(body); + + resolve(); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return a custom status message', function(done) { + expect(response.statusMessage).to.equal('OK'); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); + + it('should make sure to have the expected number of components in the fragment', function(done) { + const cardComponents = fragmentsApiDom.window.document.querySelectorAll('body > app-card'); + + expect(cardComponents.length).to.equal(2); + done(); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + runner.stopCommand(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-serve.ssr/greenwood.config.js b/packages/plugin-import-json/test/cases/exp-serve.ssr/greenwood.config.js new file mode 100644 index 000000000..f1d82b86b --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-serve.ssr/greenwood.config.js @@ -0,0 +1,7 @@ +import { greenwoodPluginImportJson } from '../../../src/index.js'; + +export default { + plugins: [ + ...greenwoodPluginImportJson() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-serve.ssr/package.json b/packages/plugin-import-json/test/cases/exp-serve.ssr/package.json new file mode 100644 index 000000000..e8ffeb80f --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-serve.ssr/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-plugin-import-json-serve-ssr", + "type": "module" +} \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-serve.ssr/src/api/fragment.js b/packages/plugin-import-json/test/cases/exp-serve.ssr/src/api/fragment.js new file mode 100644 index 000000000..5c87399f3 --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-serve.ssr/src/api/fragment.js @@ -0,0 +1,27 @@ +import { renderFromHTML } from 'wc-compiler'; +import products from '../data/products.json'; + +export async function handler() { + const { html } = await renderFromHTML(` + ${ + products.map((product) => { + const { name, thumbnail } = product; + + return ` + + `; + }).join('') + } + `, [ + new URL('../components/card.js', import.meta.url) + ]); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-serve.ssr/src/components/card.js b/packages/plugin-import-json/test/cases/exp-serve.ssr/src/components/card.js new file mode 100644 index 000000000..f87ff6dd8 --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-serve.ssr/src/components/card.js @@ -0,0 +1,26 @@ +export default class Card extends HTMLElement { + + selectItem() { + alert(`selected item is => ${this.getAttribute('title')}!`); + } + + connectedCallback() { + if (!this.shadowRoot) { + const thumbnail = this.getAttribute('thumbnail'); + const title = this.getAttribute('title'); + const template = document.createElement('template'); + + template.innerHTML = ` +
+

${title}

+ ${title} + +
+ `; + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + } +} + +customElements.define('app-card', Card); \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-serve.ssr/src/data/products.json b/packages/plugin-import-json/test/cases/exp-serve.ssr/src/data/products.json new file mode 100644 index 000000000..8cc5afbec --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-serve.ssr/src/data/products.json @@ -0,0 +1,7 @@ +[{ + "name": "iPhone 12", + "thumbnail": "iphone-12.png" +}, { + "name": "Samsung Galaxy", + "thumbnail": "samsung-galaxy.png" +}] \ No newline at end of file diff --git a/packages/plugin-import-json/test/cases/exp-serve.ssr/src/pages/products.js b/packages/plugin-import-json/test/cases/exp-serve.ssr/src/pages/products.js new file mode 100644 index 000000000..1358f8a57 --- /dev/null +++ b/packages/plugin-import-json/test/cases/exp-serve.ssr/src/pages/products.js @@ -0,0 +1,26 @@ +import '../components/card.js'; +import products from '../data/products.json'; + +export default class ProductsPage extends HTMLElement { + async connectedCallback() { + const html = products.map(product => { + const { name, thumbnail } = product; + + return ` + + + `; + }).join(''); + + this.innerHTML = ` +

SSR Page (w/ WCC)

+

List of Products: ${products.length}

+
+ ${html} +
+ `; + } +} \ No newline at end of file diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js b/packages/plugin-typescript/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js new file mode 100644 index 000000000..05af075d6 --- /dev/null +++ b/packages/plugin-typescript/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js @@ -0,0 +1,118 @@ +/* + * Use Case + * Run Greenwood with an API and SSR routes that import TypeScript. + * + * User Result + * Should generate a Greenwood build that correctly builds and bundles all assets. + * + * User Command + * greenwood build + * + * User Config + * { + * plugins: [ + * greenwoodPluginTypeScript() + * ] + * } + * + * User Workspace + * src/ + * api/ + * fragment.js + * components/ + * card.ts + */ +import chai from 'chai'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import request from 'request'; +import { Runner } from 'gallinago'; +import { fileURLToPath } from 'url'; + +const expect = chai.expect; + +describe('Serve Greenwood With: ', function() { + const LABEL = 'A Server Rendered Application (SSR) with API Routes importing TypeScript'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://127.0.0.1:8080'; + let runner; + + before(async function() { + this.context = { + publicDir: path.join(outputPath, 'public'), + hostname + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + + before(async function() { + await runner.setup(outputPath, getSetupFiles(outputPath)); + await runner.runCommand(cliPath, 'build'); + + return new Promise(async (resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + + await runner.runCommand(cliPath, 'serve'); + }); + }); + + describe('Serve command with API specific behaviors for an HTML ("fragment") API', function() { + let response = {}; + let fragmentsApiDom; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}/api/fragment`, (err, res, body) => { + if (err) { + reject(); + } + + console.log({ body }); + response = res; + response.body = body; + fragmentsApiDom = new JSDOM(body); + + resolve(); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return a custom status message', function(done) { + expect(response.statusMessage).to.equal('OK'); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); + + it('should make sure to have the expected CSS inlined into the page for each ', function(done) { + const cardComponents = fragmentsApiDom.window.document.querySelectorAll('body > app-card'); + + expect(cardComponents.length).to.equal(2); + Array.from(cardComponents).forEach((card) => { + expect(card.innerHTML).contain('font-size: 1.85rem'); + }); + done(); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + runner.stopCommand(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/greenwood.config.js b/packages/plugin-typescript/test/cases/exp-serve.ssr/greenwood.config.js new file mode 100644 index 000000000..e0f06cfb5 --- /dev/null +++ b/packages/plugin-typescript/test/cases/exp-serve.ssr/greenwood.config.js @@ -0,0 +1,7 @@ +import { greenwoodPluginTypeScript } from '../../../src/index.js'; + +export default { + plugins: [ + ...greenwoodPluginTypeScript() + ] +}; \ No newline at end of file diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/package.json b/packages/plugin-typescript/test/cases/exp-serve.ssr/package.json new file mode 100644 index 000000000..4929e3519 --- /dev/null +++ b/packages/plugin-typescript/test/cases/exp-serve.ssr/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-plugin-import-ts-serve-ssr", + "type": "module" +} \ No newline at end of file diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/src/api/fragment.js b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/api/fragment.js new file mode 100644 index 000000000..d03a8f3e6 --- /dev/null +++ b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/api/fragment.js @@ -0,0 +1,33 @@ +import { renderFromHTML } from 'wc-compiler'; + +export async function handler() { + const products = [{ + name: 'iPhone 12', + thumbnail: 'iphone-12.png' + }, { + name: 'Samsung Galaxy', + thumbnail: 'samsung-galaxy.png' + }]; + const { html } = await renderFromHTML(` + ${ + products.map((product) => { + const { name, thumbnail } = product; + + return ` + + `; + }).join('') + } + `, [ + new URL('../components/card/card.ts', import.meta.url) + ]); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/card.ts b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/card.ts new file mode 100644 index 000000000..2cad71359 --- /dev/null +++ b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/card.ts @@ -0,0 +1,33 @@ +import { styles } from './styles.ts'; + +const fallbackImage = new URL('./logo.png', import.meta.url); + +export default class Card extends HTMLElement { + + selectItem() { + alert(`selected item is => ${this.getAttribute('title')}!`); + } + + connectedCallback() { + if (!this.shadowRoot) { + const thumbnail: String = this.getAttribute('thumbnail') || fallbackImage.href; + const title: String = this.getAttribute('title'); + const template: any = document.createElement('template'); + + template.innerHTML = ` + +
+

${title}

+ ${title} + +
+ `; + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + } +} + +customElements.define('app-card', Card); \ No newline at end of file diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/logo.png b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..786360832aca0b004303207c8de025786573ff12 GIT binary patch literal 2171 zcmV->2!!{EP)9J#33pYKK;ruZV}D zb##(ls=%LpnP;cGnT4QnUxQ6?kzrYTMV+*aj;Vi)sC>iOjB}D-v(iPh&|}Qsq`uaB zV1Gtofk;<*KDN?mvC%)b)K7_~dw!Z~$=jvD*n?1VG-ZQKh@x_Ai&VAHVSbrsS9Uy{ z#!O6bFT&bsYl>8Klw!KoXSvjFd6i$j*L!b_Su>7Gc9dYO!=S|5jf${Ll)Ot#ZZWXV zR++Jhy46^%&33)lbi&$yw_WQn`=^ZHnY(~ zqr_{HwOW3lQM}h-hpAhh#!SN6gtgI8Qj=H9-k!?dp0m+Ni>7)5lk#6OX^F{#j6&f}m%g*}b5E-sBhPIfSWs4IM~$liI+Nj~UKm=xdR&is?W{SGj5+{(_((pd~1kVs(z>iAy1VV9d5vBT-*-r~uAO-`g`JLtj79lo3oE+$|=y0+w zrOl0PG7%3;bHBr(r)78*_xPe26>h$$6?!v^5Ph1y0 zNj5jriGhnnL^=W#Ze zpa^o7d*K<$V1XKv%+qZ60bDLDmPORWvYV@{Bo-CK(V3n*U#A4*Eft}PP`OJG-dSUq ztEIvZM>M3=ozXzzSB?cLxO6)z#tMeJv;HM*Pz=S=%(VrRb*tN7S!LUtAmn=S$7MEh zH_u5A5`8MM5I?kKxk;fI;taN+{4F%{r@Bz1R*?P37B5(vGl3{FLHw2(pgWqrGwmQz zfF3V`BW*f{lg{Ag!iLS1S&)85N!l(8bZkrBkNneM8q%<_46Vzh{# zh}cf==OZV*ytrR$WAFi$=T3}!KJTzaW#){??i+;~tM-mOza0J{z;;MpZEz+Pp=)s# z&z}y+-BsKK)wdVQptoRjMFZ5wK*CLh979oI_?RRt_+A>HQ2e&W3C1b*uaZau_p)R{ z%yO6$`(ekPDqBg34W%Zb>~-k%B5351^iAF3{-!Hr+iy*%Sk1+{<#?0TBcVPq_VeZ7 zN8MCVXM(d$A_}a)b;>bGjxLS!0+TjA;aSc2_ychg@qe z5Z*t1Zi&wzNboW2aNgtx*9!t0u41Db#I^=x#frCHCV{ma)7S}#wP1vqud$G7_$pHs zvdhewmp_rj7zz~?xI<+`OBashXCN-Y+!Y3X7A4;J)*~-E1PC;W;(NWqk89;lmD(7= xK5^FL^3=u%U}?JGJ$Lx02jMQk|4;Cr%RhBN)O-8hSTFzp002ovPDHLkV1iX3Cz=2N literal 0 HcmV?d00001 diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/styles.ts b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/styles.ts new file mode 100644 index 000000000..210e82df1 --- /dev/null +++ b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/components/card/styles.ts @@ -0,0 +1,7 @@ +const styles: string = ` + h3 { + font-size: 1.85rem; + } +`; + +export { styles }; \ No newline at end of file diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/src/pages/index.html b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/pages/index.html new file mode 100644 index 000000000..555a68287 --- /dev/null +++ b/packages/plugin-typescript/test/cases/exp-serve.ssr/src/pages/index.html @@ -0,0 +1,16 @@ + + + + + + + + +

Hello World!

+ + + + \ No newline at end of file diff --git a/www/pages/docs/css-and-images.md b/www/pages/docs/css-and-images.md index 657f048ee..893c01557 100644 --- a/www/pages/docs/css-and-images.md +++ b/www/pages/docs/css-and-images.md @@ -41,7 +41,7 @@ Styles can be done in any standards compliant way that will work in a browser. ### Assets -For convenience, **Greenwood** does support an "assets" directory wherein anything copied into that will be present in the build output directory. This is the recommended location for all your local images, fonts, etc. Effectively anything that is not part of an `import`, `@import`, ` + + + +

Hello World!

+ + + + \ No newline at end of file diff --git a/packages/plugin-typescript/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js b/packages/plugin-typescript/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js index 05af075d6..c38eaf820 100644 --- a/packages/plugin-typescript/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js +++ b/packages/plugin-typescript/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js @@ -73,7 +73,6 @@ describe('Serve Greenwood With: ', function() { reject(); } - console.log({ body }); response = res; response.body = body; fragmentsApiDom = new JSDOM(body); diff --git a/www/pages/docs/server-rendering.md b/www/pages/docs/server-rendering.md index eb43c1fb2..3ec770514 100644 --- a/www/pages/docs/server-rendering.md +++ b/www/pages/docs/server-rendering.md @@ -204,10 +204,10 @@ export async function getTemplate(compilation, route) { ### Custom Imports > ⚠️ _This feature is experimental._ +> +> _**Note**: At this time, [WCC can't handle non-standard javaScript formats](https://github.com/ProjectEvergreen/greenwood/issues/1004), though we hope to enable this by the 1.0 release._ -Through the support of the following plugins, Greenwood also supports loading custom file formats on the server side using ESM -- [CSS](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/plugin-import-css/README.md#usage) -- [JSON](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/plugin-import-json/README.md#usage) +Combined with Greenwood's [custom import resource plugins](https://www.greenwoodjs.io/plugins/custom-plugins/) (or your own!), Greenwood can handle loading custom file extensions on the server side using ESM, like CSS and JSON! For example, you can now import JSON in your SSR pages and components. ```js @@ -217,7 +217,7 @@ console.log(json); // { status: 200, message: 'some data' } ``` **Steps** -1. Make sure you are using Node `v18.12.1` +1. Make sure you are using Node `v18.15.0` 1. Run the Greenwood CLI using the `--experimental-loaders` flag and pass Greenwood's custom loader ```shell $ node --experimental-loader ./node_modules/@greenwood/cli/src/loader.js ./node_modules/.bin/greenwood From 34156fc54a5ecc3fda849ff4aa639631a0b15491 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 30 Oct 2023 20:37:34 -0400 Subject: [PATCH 27/35] Rfc/issue 952 data loading strategies (#1157) * add support for SSR page loader with runtime request object * refactor static export from getFrontmatter to export const prerender * document loaders and prerendering for SSR routes * adopt and document constructor props pattern for SSR page data loading * update develop SSR test case for constructor props * remove desribe.only * refactor graphql plugin for ESM compat * add test case for experimental prerendering with custom .gql imports * upgrade website for breaking changes * update website documentation and graphql plugin package README * add test cases for adapter plugins and SSR constructor props * upgrade wcc to 0.9.0 * misc PR cleanup --- packages/cli/package.json | 2 +- packages/cli/src/commands/serve.js | 2 +- packages/cli/src/lib/execute-route-module.js | 10 +- packages/cli/src/lib/resource-utils.js | 52 +++++++ packages/cli/src/lib/ssr-route-worker.js | 4 +- packages/cli/src/lifecycles/bundle.js | 9 +- packages/cli/src/lifecycles/graph.js | 21 ++- packages/cli/src/lifecycles/prerender.js | 4 +- packages/cli/src/lifecycles/serve.js | 5 +- .../src/plugins/resource/plugin-api-routes.js | 52 +------ .../plugins/resource/plugin-standard-html.js | 8 +- .../src/pages/artists.js | 5 +- .../cases/develop.ssr/develop.ssr.spec.js | 104 ++++++++++--- .../test/cases/develop.ssr/src/pages/post.js | 20 +++ .../src/pages/artists.js | 5 +- .../serve.default.ssr.spec.js | 54 ++++++- .../cases/serve.default.ssr/src/pages/post.js | 20 +++ .../cases/build.default/build.default.spec.js | 52 ++++++- .../cases/build.default/src/pages/post.js | 20 +++ .../cases/build.default/build.default.spec.js | 49 +++++- .../cases/build.default/src/pages/post.js | 20 +++ packages/plugin-graphql/README.md | 30 ++-- packages/plugin-graphql/package.json | 1 - packages/plugin-graphql/src/core/client.js | 4 +- packages/plugin-graphql/src/index.js | 28 +--- .../develop.default/develop.default.spec.js | 12 +- .../exp-prerender.query-children.spec.js | 143 ++++++++++++++++++ .../greenwood.config.js | 8 + .../exp-prerender.query-children/package.json | 4 + .../src/components/posts-list.js | 34 +++++ .../src/pages/blog/first-post/index.md | 8 + .../src/pages/blog/second-post/index.md | 8 + .../src/pages/index.html | 12 ++ .../src/components/posts-list.js | 4 +- .../query-config/src/components/footer.js | 4 +- .../src/components/posts-list.js | 2 +- .../query-custom-schema/src/pages/index.html | 2 +- .../src/components/debug-output.js | 4 +- .../cases/query-menu/src/components/header.js | 4 +- packages/plugin-import-jsx/package.json | 2 +- www/components/header/header.js | 4 +- www/components/shelf/shelf.js | 4 +- www/pages/docs/data.md | 16 +- www/pages/docs/menus.md | 4 +- www/pages/docs/server-rendering.md | 94 +++++++----- yarn.lock | 15 +- 46 files changed, 738 insertions(+), 231 deletions(-) create mode 100644 packages/cli/test/cases/develop.ssr/src/pages/post.js create mode 100644 packages/cli/test/cases/serve.default.ssr/src/pages/post.js create mode 100644 packages/plugin-adapter-netlify/test/cases/build.default/src/pages/post.js create mode 100644 packages/plugin-adapter-vercel/test/cases/build.default/src/pages/post.js create mode 100644 packages/plugin-graphql/test/cases/exp-prerender.query-children/exp-prerender.query-children.spec.js create mode 100644 packages/plugin-graphql/test/cases/exp-prerender.query-children/greenwood.config.js create mode 100644 packages/plugin-graphql/test/cases/exp-prerender.query-children/package.json create mode 100644 packages/plugin-graphql/test/cases/exp-prerender.query-children/src/components/posts-list.js create mode 100644 packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/blog/first-post/index.md create mode 100644 packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/blog/second-post/index.md create mode 100644 packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/index.html diff --git a/packages/cli/package.json b/packages/cli/package.json index 446cbf6ca..3bd29862c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,7 +52,7 @@ "remark-rehype": "^7.0.0", "rollup": "^2.58.0", "unified": "^9.2.0", - "wc-compiler": "~0.8.0" + "wc-compiler": "~0.9.0" }, "devDependencies": { "@babel/runtime": "^7.10.4", diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js index c97b3b8c2..b48662e46 100644 --- a/packages/cli/src/commands/serve.js +++ b/packages/cli/src/commands/serve.js @@ -8,7 +8,7 @@ const runProdServer = async (compilation) => { try { const port = compilation.config.port; const hasApisDir = await checkResourceExists(compilation.context.apisDir); - const hasDynamicRoutes = compilation.graph.find(page => page.isSSR && !page.data.static); + const hasDynamicRoutes = compilation.graph.find(page => page.isSSR && !page.prerender); const server = (hasDynamicRoutes && !compilation.config.prerender) || hasApisDir ? getHybridServer : getStaticServer; (await server(compilation)).listen(port, () => { diff --git a/packages/cli/src/lib/execute-route-module.js b/packages/cli/src/lib/execute-route-module.js index 5b36631fe..483696fdc 100644 --- a/packages/cli/src/lib/execute-route-module.js +++ b/packages/cli/src/lib/execute-route-module.js @@ -1,6 +1,6 @@ import { renderToString, renderFromHTML } from 'wc-compiler'; -async function executeRouteModule({ moduleUrl, compilation, page = {}, prerender = false, htmlContents = null, scripts = [] }) { +async function executeRouteModule({ moduleUrl, compilation, page = {}, prerender = false, htmlContents = null, scripts = [], request }) { const data = { template: null, body: null, @@ -15,15 +15,15 @@ async function executeRouteModule({ moduleUrl, compilation, page = {}, prerender data.html = html; } else { const module = await import(moduleUrl).then(module => module); - const { getTemplate = null, getBody = null, getFrontmatter = null } = module; + const { prerender = false, getTemplate = null, getBody = null, getFrontmatter = null } = module; if (module.default) { - const { html } = await renderToString(new URL(moduleUrl), false); + const { html } = await renderToString(new URL(moduleUrl), false, request); data.body = html; } else { if (getBody) { - data.body = await getBody(compilation, page); + data.body = await getBody(compilation, page, request); } } @@ -34,6 +34,8 @@ async function executeRouteModule({ moduleUrl, compilation, page = {}, prerender if (getFrontmatter) { data.frontmatter = await getFrontmatter(compilation, page); } + + data.prerender = prerender; } return data; diff --git a/packages/cli/src/lib/resource-utils.js b/packages/cli/src/lib/resource-utils.js index 410c13d60..a3d71e579 100644 --- a/packages/cli/src/lib/resource-utils.js +++ b/packages/cli/src/lib/resource-utils.js @@ -208,11 +208,63 @@ function transformKoaRequestIntoStandardRequest(url, request) { }); } +// https://stackoverflow.com/questions/57447685/how-can-i-convert-a-request-object-into-a-stringifiable-object-in-javascript +async function requestAsObject (_request) { + if (!_request instanceof Request) { + throw Object.assign( + new Error(), + { name: 'TypeError', message: 'Argument must be a Request object' } + ); + } + + const request = _request.clone(); + const contentType = request.headers.get('content-type') || ''; + let headers = Object.fromEntries(request.headers); + let format; + + function stringifiableObject (obj) { + const filtered = {}; + for (const key in obj) { + if (['boolean', 'number', 'string'].includes(typeof obj[key]) || obj[key] === null) { + filtered[key] = obj[key]; + } + } + return filtered; + } + + if (contentType.includes('application/x-www-form-urlencoded')) { + const formData = await request.formData(); + const params = {}; + + for (const entry of formData.entries()) { + params[entry[0]] = entry[1]; + } + + // when using FormData, let Request set the correct headers + // or else it will come out as multipart/form-data + // for serialization between route workers, leave a special marker for Greenwood + // https://stackoverflow.com/a/43521052/417806 + headers['content-type'] = 'x-greenwood/www-form-urlencoded'; + format = JSON.stringify(params); + } else if (contentType.includes('application/json')) { + format = JSON.stringify(await request.json()); + } else { + format = await request.text(); + } + + return { + ...stringifiableObject(request), + body: format, + headers + }; +} + export { checkResourceExists, mergeResponse, modelResource, normalizePathnameForWindows, + requestAsObject, resolveForRelativeUrl, trackResourcesForRoute, transformKoaRequestIntoStandardRequest diff --git a/packages/cli/src/lib/ssr-route-worker.js b/packages/cli/src/lib/ssr-route-worker.js index 12eb71d88..239eb49a3 100644 --- a/packages/cli/src/lib/ssr-route-worker.js +++ b/packages/cli/src/lib/ssr-route-worker.js @@ -1,9 +1,9 @@ // https://github.com/nodejs/modules/issues/307#issuecomment-858729422 import { parentPort } from 'worker_threads'; -async function executeModule({ executeModuleUrl, moduleUrl, compilation, page, prerender = false, htmlContents = null, scripts = '[]' }) { +async function executeModule({ executeModuleUrl, moduleUrl, compilation, page, prerender = false, htmlContents = null, scripts = '[]', request }) { const { executeRouteModule } = await import(executeModuleUrl); - const data = await executeRouteModule({ moduleUrl, compilation: JSON.parse(compilation), page: JSON.parse(page), prerender, htmlContents, scripts: JSON.parse(scripts) }); + const data = await executeRouteModule({ moduleUrl, compilation: JSON.parse(compilation), page: JSON.parse(page), prerender, htmlContents, scripts: JSON.parse(scripts), request }); parentPort.postMessage(data); } diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index e931847f6..783b84260 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -44,7 +44,7 @@ async function optimizeStaticPages(compilation, plugins) { const { scratchDir, outputDir } = compilation.context; return Promise.all(compilation.graph - .filter(page => !page.isSSR || (page.isSSR && page.data.static) || (page.isSSR && compilation.config.prerender)) + .filter(page => !page.isSSR || (page.isSSR && page.prerender) || (page.isSSR && compilation.config.prerender)) .map(async (page) => { const { route, outputPath } = page; const outputDirUrl = new URL(`.${route}`, outputDir); @@ -189,13 +189,14 @@ async function bundleSsrPages(compilation) { const { pagesDir, scratchDir } = compilation.context; for (const page of compilation.graph) { - if (page.isSSR && !page.data.static) { + if (page.isSSR && !page.prerender) { const { filename, imports, route, template, title } = page; const entryFileUrl = new URL(`./_${filename}`, scratchDir); const moduleUrl = new URL(`./${filename}`, pagesDir); + const request = new Request(moduleUrl); // TODO not really sure how to best no-op this? // TODO getTemplate has to be static (for now?) // https://github.com/ProjectEvergreen/greenwood/issues/955 - const data = await executeRouteModule({ moduleUrl, compilation, page, prerender: false, htmlContents: null, scripts: [] }); + const data = await executeRouteModule({ moduleUrl, compilation, page, prerender: false, htmlContents: null, scripts: [], request }); let staticHtml = ''; staticHtml = data.template ? data.template : await getPageTemplate(staticHtml, compilation.context, template, []); @@ -212,7 +213,7 @@ async function bundleSsrPages(compilation) { const compilation = JSON.parse('${JSON.stringify(compilation)}'); const page = JSON.parse('${JSON.stringify(page)}'); const moduleUrl = '___GWD_ENTRY_FILE_URL=${filename}___'; - const data = await executeRouteModule({ moduleUrl, compilation, page }); + const data = await executeRouteModule({ moduleUrl, compilation, page, request }); let staticHtml = \`${staticHtml}\`; if (data.body) { diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js index 486adb6d1..ea8fd1794 100644 --- a/packages/cli/src/lifecycles/graph.js +++ b/packages/cli/src/lifecycles/graph.js @@ -1,7 +1,7 @@ /* eslint-disable complexity, max-depth */ import fs from 'fs/promises'; import fm from 'front-matter'; -import { checkResourceExists } from '../lib/resource-utils.js'; +import { checkResourceExists, requestAsObject } from '../lib/resource-utils.js'; import toc from 'markdown-toc'; import { Worker } from 'worker_threads'; @@ -20,7 +20,8 @@ const generateGraph = async (compilation) => { label: 'Index', data: {}, imports: [], - resources: [] + resources: [], + prerender: true }]; const walkDirectoryForPages = async function(directory, pages = []) { @@ -46,6 +47,7 @@ const generateGraph = async (compilation) => { let imports = []; let customData = {}; let filePath; + let prerender = true; /* * check if additional nested directories exist to correctly determine route (minus filename) @@ -121,14 +123,19 @@ const generateGraph = async (compilation) => { filePath = route; - await new Promise((resolve, reject) => { + await new Promise(async (resolve, reject) => { const worker = new Worker(new URL('../lib/ssr-route-worker.js', import.meta.url)); + // TODO "faux" new Request here, a better way? + const request = await requestAsObject(new Request(filenameUrl)); worker.on('message', async (result) => { + prerender = result.prerender; + if (result.frontmatter) { result.frontmatter.imports = result.frontmatter.imports || []; ssrFrontmatter = result.frontmatter; } + resolve(); }); worker.on('error', reject); @@ -151,7 +158,8 @@ const generateGraph = async (compilation) => { .map((idPart) => { return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; }).join(' ') - }) + }), + request }); }); @@ -190,6 +198,8 @@ const generateGraph = async (compilation) => { * route: URL route for a given page on outputFilePath * template: page template to use as a base for a generated component * title: a default value that can be used for + * isSSR: if this is a server side route + * prerednder: if this should be statically exported */ pages.push({ data: customData || {}, @@ -208,7 +218,8 @@ const generateGraph = async (compilation) => { route, template, title, - isSSR: !isStatic + isSSR: !isStatic, + prerender }); } } diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 5f4caf964..56c655159 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -49,7 +49,7 @@ function getPluginInstances (compilation) { } async function preRenderCompilationWorker(compilation, workerPrerender) { - const pages = compilation.graph.filter(page => !page.isSSR || (page.isSSR && page.data.static) || (page.isSSR && compilation.config.prerender)); + const pages = compilation.graph.filter(page => !page.isSSR || (page.isSSR && page.prerender) || (page.isSSR && compilation.config.prerender)); const { scratchDir } = compilation.context; const plugins = getPluginInstances(compilation); @@ -128,7 +128,7 @@ async function preRenderCompilationCustom(compilation, customPrerender) { async function staticRenderCompilation(compilation) { const { scratchDir } = compilation.context; - const pages = compilation.graph.filter(page => !page.isSSR || page.isSSR && page.data.static); + const pages = compilation.graph.filter(page => !page.isSSR || page.isSSR && page.prerender); const plugins = getPluginInstances(compilation); console.info('pages to generate', `\n ${pages.map(page => page.route).join('\n ')}`); diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index da386eaaa..bb7c66c48 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -179,7 +179,7 @@ async function getStaticServer(compilation, composable) { const matchingRoute = compilation.graph.find(page => page.route === url.pathname); const isSPA = compilation.graph.find(page => page.isSPA); const { isSSR } = matchingRoute || {}; - const isStatic = matchingRoute && !isSSR || isSSR && compilation.config.prerender || isSSR && matchingRoute.data.static; + const isStatic = matchingRoute && !isSSR || isSSR && compilation.config.prerender || isSSR && matchingRoute.prerender; if (isSPA || (matchingRoute && isStatic) || url.pathname.split('.').pop() === 'html') { const pathname = isSPA @@ -293,9 +293,8 @@ async function getHybridServer(compilation) { const isApiRoute = manifest.apis.has(url.pathname); const request = transformKoaRequestIntoStandardRequest(url, ctx.request); - if (!config.prerender && matchingRoute.isSSR && !matchingRoute.data.static) { + if (!config.prerender && matchingRoute.isSSR && !matchingRoute.prerender) { const { handler } = await import(new URL(`./__${matchingRoute.filename}`, outputDir)); - // TODO passing compilation this way too hacky? const response = await handler(request, compilation); ctx.body = Readable.from(response.body); diff --git a/packages/cli/src/plugins/resource/plugin-api-routes.js b/packages/cli/src/plugins/resource/plugin-api-routes.js index dcb023eb0..6228f3e97 100644 --- a/packages/cli/src/plugins/resource/plugin-api-routes.js +++ b/packages/cli/src/plugins/resource/plugin-api-routes.js @@ -4,59 +4,9 @@ * */ import { ResourceInterface } from '../../lib/resource-interface.js'; +import { requestAsObject } from '../../lib/resource-utils.js'; import { Worker } from 'worker_threads'; -// https://stackoverflow.com/questions/57447685/how-can-i-convert-a-request-object-into-a-stringifiable-object-in-javascript -async function requestAsObject (_request) { - if (!_request instanceof Request) { - throw Object.assign( - new Error(), - { name: 'TypeError', message: 'Argument must be a Request object' } - ); - } - - const request = _request.clone(); - const contentType = request.headers.get('content-type') || ''; - let headers = Object.fromEntries(request.headers); - let format; - - function stringifiableObject (obj) { - const filtered = {}; - for (const key in obj) { - if (['boolean', 'number', 'string'].includes(typeof obj[key]) || obj[key] === null) { - filtered[key] = obj[key]; - } - } - return filtered; - } - - if (contentType.includes('application/x-www-form-urlencoded')) { - const formData = await request.formData(); - const params = {}; - - for (const entry of formData.entries()) { - params[entry[0]] = entry[1]; - } - - // when using FormData, let Request set the correct headers - // or else it will come out as multipart/form-data - // for serialization between route workers, leave a special marker for Greenwood - // https://stackoverflow.com/a/43521052/417806 - headers['content-type'] = 'x-greenwood/www-form-urlencoded'; - format = JSON.stringify(params); - } else if (contentType.includes('application/json')) { - format = JSON.stringify(await request.json()); - } else { - format = await request.text(); - } - - return { - ...stringifiableObject(request), - body: format, - headers - }; -} - class ApiRoutesResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 4b91d69bc..933893d62 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -14,6 +14,7 @@ import remarkParse from 'remark-parse'; import remarkRehype from 'remark-rehype'; import { ResourceInterface } from '../../lib/resource-interface.js'; import { getUserScripts, getPageTemplate, getAppTemplate } from '../../lib/templating-utils.js'; +import { requestAsObject } from '../../lib/resource-utils.js'; import unified from 'unified'; import { Worker } from 'worker_threads'; @@ -33,7 +34,7 @@ class StandardHtmlResource extends ResourceInterface { return protocol.startsWith('http') && (hasMatchingPageRoute || isSPA); } - async serve(url) { + async serve(url, request) { const { config, context } = this.compilation; const { pagesDir, userWorkspace } = context; const { interpolateFrontmatter } = config; @@ -107,7 +108,7 @@ class StandardHtmlResource extends ResourceInterface { const routeModuleLocationUrl = new URL(`./${matchingRoute.filename}`, pagesDir); const routeWorkerUrl = this.compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().executeModuleUrl; - await new Promise((resolve, reject) => { + await new Promise(async (resolve, reject) => { const worker = new Worker(new URL('../../lib/ssr-route-worker.js', import.meta.url)); worker.on('message', (result) => { @@ -146,7 +147,8 @@ class StandardHtmlResource extends ResourceInterface { executeModuleUrl: routeWorkerUrl.href, moduleUrl: routeModuleLocationUrl.href, compilation: JSON.stringify(this.compilation), - page: JSON.stringify(matchingRoute) + page: JSON.stringify(matchingRoute), + request: await requestAsObject(request) }); }); } diff --git a/packages/cli/test/cases/build.default.ssr-static-export/src/pages/artists.js b/packages/cli/test/cases/build.default.ssr-static-export/src/pages/artists.js index e0d3773e5..af4f03ed3 100644 --- a/packages/cli/test/cases/build.default.ssr-static-export/src/pages/artists.js +++ b/packages/cli/test/cases/build.default.ssr-static-export/src/pages/artists.js @@ -77,12 +77,13 @@ async function getFrontmatter() { ], data: { author: 'Project Evergreen', - date: '01-01-2021', - static: true + date: '01-01-2021' } }; } +export const prerender = true; + export { getTemplate, getBody, diff --git a/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js index 830079fdf..d0fac0374 100644 --- a/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js +++ b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js @@ -17,6 +17,7 @@ * footer.js * pages/ * artists.js + * post.js * templates/ * app.html */ @@ -134,31 +135,30 @@ describe('Develop Greenwood With: ', function() { }); }); - let response = {}; - let dom; - let artistsPageGraphData; + describe('Develop command with HTML route response using getTemplate, getBody, getFrontmatter', function() { + let response = {}; + let dom; + let artistsPageGraphData; - before(async function() { - const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, '.greenwood/graph.json'), 'utf-8')); + before(async function() { + const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, '.greenwood/graph.json'), 'utf-8')); - artistsPageGraphData = graph.filter(page => page.route === '/artists/')[0]; + artistsPageGraphData = graph.filter(page => page.route === '/artists/')[0]; - return new Promise((resolve, reject) => { - request.get(`${hostname}/artists/`, (err, res, body) => { - if (err) { - reject(); - } + return new Promise((resolve, reject) => { + request.get(`${hostname}/artists/`, (err, res, body) => { + if (err) { + reject(); + } - response = res; - response.body = body; - dom = new JSDOM(body); + response = res; + response.body = body; + dom = new JSDOM(body); - resolve(); + resolve(); + }); }); }); - }); - - describe('Serve command with HTML route response', function() { it('should return a 200 status', function(done) { expect(response.statusCode).to.equal(200); @@ -236,6 +236,74 @@ describe('Develop Greenwood With: ', function() { expect(counterScript.length).to.equal(1); }); }); + + describe('Develop command with HTML route response using default export and request time data', function() { + const postId = 1; + let response = {}; + let dom = {}; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}/post/?id=${postId}`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + dom = new JSDOM(body); + + resolve(); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(response.body).to.not.be.undefined; + done(); + }); + + it('should be valid HTML from JSDOM', function(done) { + expect(dom).to.not.be.undefined; + done(); + }); + + it('should be valid HTML from JSDOM', function(done) { + expect(dom).to.not.be.undefined; + done(); + }); + + it('should have the expected postId as an

tag in the body', function() { + const heading = dom.window.document.querySelectorAll('body > h1'); + + expect(heading.length).to.equal(1); + expect(heading[0].textContent).to.equal(`Fetched Post ID: ${postId}`); + }); + + it('should have the expected title as an

tag in the body', function() { + const heading = dom.window.document.querySelectorAll('body > h2'); + + expect(heading.length).to.equal(1); + expect(heading[0].textContent).to.not.be.undefined; + }); + + it('should have the expected body as a

tag in the body', function() { + const paragraph = dom.window.document.querySelectorAll('body > p'); + + expect(paragraph.length).to.equal(1); + expect(paragraph[0].textContent).to.not.be.undefined; + }); + }); }); after(function() { diff --git a/packages/cli/test/cases/develop.ssr/src/pages/post.js b/packages/cli/test/cases/develop.ssr/src/pages/post.js new file mode 100644 index 000000000..a5d7d10d6 --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/src/pages/post.js @@ -0,0 +1,20 @@ +export default class PostPage extends HTMLElement { + constructor(request) { + super(); + + const params = new URLSearchParams(request.url.slice(request.url.indexOf('?'))); + this.postId = params.get('id'); + } + + async connectedCallback() { + const { postId } = this; + const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then(resp => resp.json()); + const { id, title, body } = post; + + this.innerHTML = ` +

Fetched Post ID: ${id}

+

${title}

+

${body}

+ `; + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default.ssr-static-export/src/pages/artists.js b/packages/cli/test/cases/serve.default.ssr-static-export/src/pages/artists.js index e0d3773e5..af4f03ed3 100644 --- a/packages/cli/test/cases/serve.default.ssr-static-export/src/pages/artists.js +++ b/packages/cli/test/cases/serve.default.ssr-static-export/src/pages/artists.js @@ -77,12 +77,13 @@ async function getFrontmatter() { ], data: { author: 'Project Evergreen', - date: '01-01-2021', - static: true + date: '01-01-2021' } }; } +export const prerender = true; + export { getTemplate, getBody, diff --git a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js index bf60ae24c..5d6a49233 100644 --- a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js +++ b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js @@ -24,6 +24,7 @@ * about.md * artists.js * index.js + * post.js * users.js * templates/ * app.html @@ -286,14 +287,6 @@ describe('Serve Greenwood With: ', function() { expect(cards.length).to.be.greaterThan(0); }); - xit('should have a bundled + + + + + + + \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/query-children/src/components/posts-list.js b/packages/plugin-graphql/test/cases/query-children/src/components/posts-list.js index 72f782830..7d834bb06 100644 --- a/packages/plugin-graphql/test/cases/query-children/src/components/posts-list.js +++ b/packages/plugin-graphql/test/cases/query-children/src/components/posts-list.js @@ -1,6 +1,6 @@ import { html, LitElement } from 'lit'; -import client from '@greenwood/plugin-graphql/core/client'; -import ChildrenQuery from '@greenwood/plugin-graphql/queries/children'; +import client from '@greenwood/plugin-graphql/src/core/client.js'; +import ChildrenQuery from '@greenwood/plugin-graphql/src/queries/children.gql'; class PostsListTemplate extends LitElement { diff --git a/packages/plugin-graphql/test/cases/query-config/src/components/footer.js b/packages/plugin-graphql/test/cases/query-config/src/components/footer.js index 94c21ac6a..4d5b83d23 100644 --- a/packages/plugin-graphql/test/cases/query-config/src/components/footer.js +++ b/packages/plugin-graphql/test/cases/query-config/src/components/footer.js @@ -1,5 +1,5 @@ -import client from '@greenwood/plugin-graphql/core/client'; -import ConfigQuery from '@greenwood/plugin-graphql/queries/config'; +import client from '@greenwood/plugin-graphql/src/core/client.js'; +import ConfigQuery from '@greenwood/plugin-graphql/src/queries/config.gql'; class FooterComponent extends HTMLElement { constructor() { diff --git a/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/components/posts-list.js b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/components/posts-list.js index f7f37ecde..06fdaf760 100644 --- a/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/components/posts-list.js +++ b/packages/plugin-graphql/test/cases/query-custom-frontmatter/src/components/posts-list.js @@ -1,5 +1,5 @@ import { html, LitElement } from 'lit'; -import client from '@greenwood/plugin-graphql/core/client'; +import client from '@greenwood/plugin-graphql/src/core/client.js'; class PostsListTemplate extends LitElement { diff --git a/packages/plugin-graphql/test/cases/query-custom-schema/src/pages/index.html b/packages/plugin-graphql/test/cases/query-custom-schema/src/pages/index.html index 6f6ac74f2..3d0ce48d9 100644 --- a/packages/plugin-graphql/test/cases/query-custom-schema/src/pages/index.html +++ b/packages/plugin-graphql/test/cases/query-custom-schema/src/pages/index.html @@ -3,7 +3,7 @@ + `); + // TODO get rid of lit polyfills in core // https://github.com/ProjectEvergreen/greenwood/issues/728 // https://lit.dev/docs/tools/requirements/#polyfills diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index 783b84260..e5a28fdd5 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -47,7 +47,7 @@ async function optimizeStaticPages(compilation, plugins) { .filter(page => !page.isSSR || (page.isSSR && page.prerender) || (page.isSSR && compilation.config.prerender)) .map(async (page) => { const { route, outputPath } = page; - const outputDirUrl = new URL(`.${route}`, outputDir); + const outputDirUrl = new URL(`.${outputPath.replace('index.html', '').replace('404.html', '')}`, outputDir); const url = new URL(`http://localhost:${compilation.config.port}${route}`); const contents = await fs.readFile(new URL(`./${outputPath}`, scratchDir), 'utf-8'); const headers = new Headers({ 'Content-Type': 'text/html' }); @@ -70,7 +70,7 @@ async function optimizeStaticPages(compilation, plugins) { // clean up optimization markers const body = (await response.text()).replace(/data-gwd-opt=".*[a-z]"/g, ''); - await fs.writeFile(new URL(`./${outputPath}`, outputDir), body); + await fs.writeFile(new URL(`.${outputPath}`, outputDir), body); }) ); } @@ -201,7 +201,7 @@ async function bundleSsrPages(compilation) { staticHtml = data.template ? data.template : await getPageTemplate(staticHtml, compilation.context, template, []); staticHtml = await getAppTemplate(staticHtml, compilation.context, imports, [], false, title); - staticHtml = await getUserScripts(staticHtml, compilation.context); + staticHtml = await getUserScripts(staticHtml, compilation); staticHtml = await (await htmlOptimizer.optimize(new URL(`http://localhost:8080${route}`), new Response(staticHtml))).text(); staticHtml = staticHtml.replace(/[`\\$]/g, '\\$&'); // https://stackoverflow.com/a/75688937/417806 diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js index dd8bff451..e9b9dd400 100644 --- a/packages/cli/src/lifecycles/config.js +++ b/packages/cli/src/lifecycles/config.js @@ -44,6 +44,7 @@ const defaultConfig = { extensions: [] }, port: 8080, + basePath: '', optimization: optimizations[0], interpolateFrontmatter: false, plugins: greenwoodPlugins, @@ -75,7 +76,7 @@ const readAndMergeConfig = async() => { if (hasConfigFile) { const userCfgFile = (await import(configUrl)).default; - const { workspace, devServer, markdown, optimization, plugins, port, prerender, staticRouter, pagesDirectory, templatesDirectory, interpolateFrontmatter } = userCfgFile; + const { workspace, devServer, markdown, optimization, plugins, port, prerender, basePath, staticRouter, pagesDirectory, templatesDirectory, interpolateFrontmatter } = userCfgFile; // workspace validation if (workspace) { @@ -188,6 +189,15 @@ const readAndMergeConfig = async() => { } } + if (basePath) { + // eslint-disable-next-line max-depth + if (typeof basePath !== 'string') { + reject(`Error: greenwood.config.js basePath must be a string. Passed value was: ${basePath}`); + } else { + customConfig.basePath = basePath; + } + } + if (pagesDirectory && typeof pagesDirectory === 'string') { customConfig.pagesDirectory = pagesDirectory; } else if (pagesDirectory) { diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js index ea8fd1794..ae82285e6 100644 --- a/packages/cli/src/lifecycles/graph.js +++ b/packages/cli/src/lifecycles/graph.js @@ -9,13 +9,14 @@ const generateGraph = async (compilation) => { return new Promise(async (resolve, reject) => { try { - const { context } = compilation; + const { context, config } = compilation; + const { basePath } = config; const { apisDir, pagesDir, projectDirectory, userWorkspace } = context; let graph = [{ - outputPath: 'index.html', + outputPath: '/index.html', filename: 'index.html', path: '/', - route: '/', + route: `${basePath}/`, id: 'index', label: 'Index', data: {}, @@ -212,10 +213,10 @@ const generateGraph = async (compilation) => { imports, resources: [], outputPath: route === '/404/' - ? '404.html' + ? '/404.html' : `${route}index.html`, path: filePath, - route, + route: `${basePath}${route}`, template, title, isSSR: !isStatic, @@ -240,7 +241,7 @@ const generateGraph = async (compilation) => { } else { const extension = filenameUrl.pathname.split('.').pop(); const relativeApiPath = filenameUrl.pathname.replace(userWorkspace.pathname, '/'); - const route = relativeApiPath.replace(`.${extension}`, ''); + const route = `${basePath}${relativeApiPath.replace(`.${extension}`, '')}`; if (extension !== 'js') { console.warn(`${filenameUrl} is not a JavaScript file, skipping...`); @@ -280,7 +281,7 @@ const generateGraph = async (compilation) => { graph = await checkResourceExists(pagesDir) ? await walkDirectoryForPages(pagesDir) : graph; - const has404Page = graph.filter(page => page.route === '/404/').length === 1; + const has404Page = graph.find(page => page.route.endsWith('/404/')); // if the _only_ page is a 404 page, still provide a default index.html if (has404Page && graph.length === 1) { @@ -293,9 +294,9 @@ const generateGraph = async (compilation) => { ...graph, { ...oldGraph, - outputPath: '404.html', + outputPath: '/404.html', filename: '404.html', - route: '/404/', + route: `${basePath}/404/`, path: '404.html', id: '404', label: 'Not Found' diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 56c655159..6eb1a3438 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -6,7 +6,7 @@ import { WorkerPool } from '../lib/threadpool.js'; // TODO a lot of these are duplicated in the build lifecycle too // would be good to refactor async function createOutputDirectory(route, outputDir) { - if (route !== '/404/' && !await checkResourceExists(outputDir)) { + if (!route.endsWith('/404/') && !await checkResourceExists(outputDir)) { await fs.mkdir(outputDir, { recursive: true }); @@ -59,15 +59,14 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { for (const page of pages) { const { route, outputPath, resources } = page; - const outputDirUrl = new URL(`./${route}/`, scratchDir); - const outputPathUrl = new URL(`./${outputPath}`, scratchDir); + const outputPathUrl = new URL(`.${outputPath}`, scratchDir); const url = new URL(`http://localhost:${compilation.config.port}${route}`); const request = new Request(url); let body = await (await servePage(url, request, plugins)).text(); body = await (await interceptPage(url, request, plugins, body)).text(); - await createOutputDirectory(route, outputDirUrl); + await createOutputDirectory(route, new URL(outputPathUrl.href.replace('index.html', ''))); const scripts = resources .map(resource => compilation.resources.get(resource)) @@ -106,8 +105,7 @@ async function preRenderCompilationCustom(compilation, customPrerender) { await renderer(compilation, async (page, body) => { const { route, outputPath } = page; - const outputDirUrl = new URL(`./${route}`, scratchDir); - const outputPathUrl = new URL(`./${outputPath}`, scratchDir); + const outputPathUrl = new URL(`.${outputPath}`, scratchDir); // clean up special Greenwood dev only assets that would come through if prerendering with a headless browser body = body.replace(/`, ` `); } else if (optimizationAttr === 'static' || optimization === 'static') { @@ -252,7 +252,7 @@ class StandardHtmlResource extends ResourceInterface { } } else if (type === 'link') { if (!optimizationAttr && (optimization !== 'none' && optimization !== 'inline')) { - const optimizedFilePath = `/${optimizedFileName}`; + const optimizedFilePath = `${basePath}/${optimizedFileName}`; body = body.replace(src, optimizedFilePath); body = body.replace('', ` @@ -280,9 +280,9 @@ class StandardHtmlResource extends ResourceInterface { if (optimizationAttr === 'static' || optimization === 'static') { body = body.replace(``, ''); } else if (optimizationAttr === 'none') { - body = body.replace(contents, contents.replace(/\.\//g, '/').replace(/\$/g, '$$$')); + body = body.replace(contents, contents.replace(/\.\//g, `${basePath}/`).replace(/\$/g, '$$$')); } else { - body = body.replace(contents, optimizedFileContents.replace(/\.\//g, '/').replace(/\$/g, '$$$')); + body = body.replace(contents, optimizedFileContents.replace(/\.\//g, `${basePath}/`).replace(/\$/g, '$$$')); } } else if (type === 'style') { body = body.replace(contents, optimizedFileContents); diff --git a/packages/cli/src/plugins/resource/plugin-static-router.js b/packages/cli/src/plugins/resource/plugin-static-router.js index 48af82277..90b144d12 100644 --- a/packages/cli/src/plugins/resource/plugin-static-router.js +++ b/packages/cli/src/plugins/resource/plugin-static-router.js @@ -56,30 +56,31 @@ class StaticRouterResource extends ResourceInterface { async optimize(url, response) { let body = await response.text(); + const { basePath } = this.compilation.config; const { pathname } = url; const isStaticRoute = this.compilation.graph.find(page => page.route === pathname && !page.isSSR); const { outputDir } = this.compilation.context; const partial = body.match(/(.*)<\/body>/s)[0].replace('', '').replace('', ''); - const outputPartialDirUrl = new URL(`./_routes${url.pathname}`, outputDir); + const outputPartialDirUrl = new URL(`./_routes${url.pathname.replace(basePath, '')}`, outputDir); const outputPartialDirPathUrl = new URL(`file://${outputPartialDirUrl.pathname.split('/').slice(0, -1).join('/').concat('/')}`); let currentTemplate; const routeTags = this.compilation.graph .filter(page => !page.isSSR) - .filter(page => page.route !== '/404/') + .filter(page => !page.route.endsWith('/404/')) .map((page) => { const template = page.filename && page.filename.split('.').pop() === this.extensions[0] ? page.route : page.template; const key = page.route === '/' ? '' - : page.route.slice(0, page.route.lastIndexOf('/')); + : page.route.slice(0, page.route.lastIndexOf('/')).replace(basePath, ''); if (pathname === page.route) { currentTemplate = template; } return ` - + `; }); @@ -93,13 +94,14 @@ class StaticRouterResource extends ResourceInterface { await fs.writeFile(new URL('./index.html', outputPartialDirUrl), partial); } - body = body.replace('', ` - - - `.replace(/\n/g, '').replace(/ /g, '')) + body = body + .replace('', ` + + + `) .replace(/(.*)<\/body>/s, ` \n 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 4c6a66793..6a3aa9372 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 @@ -72,7 +72,7 @@ describe('Build Greenwood With: ', function() { }); it('should have the expected tag in the with mixed attribute ordering', function() { it('should have two + + + + +

Hello World

+ +

Greenwood Logo

+ Greenwood Logo +
+ + + \ No newline at end of file diff --git a/packages/cli/test/cases/develop.config.base-path/src/pages/users.js b/packages/cli/test/cases/develop.config.base-path/src/pages/users.js new file mode 100644 index 000000000..7497339d8 --- /dev/null +++ b/packages/cli/test/cases/develop.config.base-path/src/pages/users.js @@ -0,0 +1,18 @@ +export default class UsersPage extends HTMLElement { + async connectedCallback() { + const users = await fetch('https://www.analogstudios.net/api/artists').then(resp => resp.json()); + const html = users.map(user => { + return ` +
+

${user.name}

+ ${user.name} +
+ `; + }).join(''); + + this.innerHTML = ` +

List of Users: ${users.length}

+ ${html} + `; + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/develop.config.base-path/src/styles/main.css b/packages/cli/test/cases/develop.config.base-path/src/styles/main.css new file mode 100644 index 000000000..9a7b45f93 --- /dev/null +++ b/packages/cli/test/cases/develop.config.base-path/src/styles/main.css @@ -0,0 +1,3 @@ +* { + color: blue; +} \ No newline at end of file diff --git a/packages/cli/test/cases/develop.default/develop.default.spec.js b/packages/cli/test/cases/develop.default/develop.default.spec.js index d6845298e..e31d2466e 100644 --- a/packages/cli/test/cases/develop.default/develop.default.spec.js +++ b/packages/cli/test/cases/develop.default/develop.default.spec.js @@ -552,6 +552,17 @@ describe('Develop Greenwood With: ', function() { done(); }); + + it('should add a + + + + +

Hello World

+ +

Greenwood Logo

+ Greenwood Logo +
+ + + \ No newline at end of file diff --git a/packages/cli/test/cases/serve.config.base-path/src/pages/users.js b/packages/cli/test/cases/serve.config.base-path/src/pages/users.js new file mode 100644 index 000000000..7497339d8 --- /dev/null +++ b/packages/cli/test/cases/serve.config.base-path/src/pages/users.js @@ -0,0 +1,18 @@ +export default class UsersPage extends HTMLElement { + async connectedCallback() { + const users = await fetch('https://www.analogstudios.net/api/artists').then(resp => resp.json()); + const html = users.map(user => { + return ` +
+

${user.name}

+ ${user.name} +
+ `; + }).join(''); + + this.innerHTML = ` +

List of Users: ${users.length}

+ ${html} + `; + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/serve.config.base-path/src/styles/main.css b/packages/cli/test/cases/serve.config.base-path/src/styles/main.css new file mode 100644 index 000000000..9a7b45f93 --- /dev/null +++ b/packages/cli/test/cases/serve.config.base-path/src/styles/main.css @@ -0,0 +1,3 @@ +* { + color: blue; +} \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js index 2b412e42b..87c535b13 100644 --- a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js +++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js @@ -123,13 +123,13 @@ describe('Serve Greenwood With: ', function() { // TODO no page.js output describe('Serve command for static HTML response with bundled home page +``` + +For convenience, the value of `basePath` will also be made available as a global variable in the `` of your pages. For example: +```html + +``` + +> _User content, like `` and `` tags will require manually prefixing the basePath in your code._ + ### Dev Server Configuration for Greenwood's development server is available using the `devServer` option. - `extensions`: Provide an array of extensions to watch for changes and reload the live server with. By default, Greenwood will already watch all "standard" web assets (HTML, CSS, JS, etc) it supports by default, as well as any extensions set by [resource plugins](/plugins/resource) you are using in your _greenwood.config.json_. diff --git a/www/pages/docs/data.md b/www/pages/docs/data.md index d9423484a..530afc393 100644 --- a/www/pages/docs/data.md +++ b/www/pages/docs/data.md @@ -192,7 +192,7 @@ async connectedCallback() { const response = await client.query({ query: ChildrenQuery, variables: { - parent: 'blog' + parent: '/blog' } }); diff --git a/www/pages/guides/github-pages.md b/www/pages/guides/github-pages.md index ada8c22cc..5bb06df73 100644 --- a/www/pages/guides/github-pages.md +++ b/www/pages/guides/github-pages.md @@ -14,7 +14,8 @@ In this guide we'll walk through the steps for setting up a [GitHub Pages](https ### Prerequisites Following the steps [outlined here](https://pages.github.com/), first make sure you have already: -1. Created a repo in the format `.github.io` +1. Created a repo in the format `.github.io` or `.github.io/` + - If using `.github.io/`, make sure to set Greenwood's [base path](/docs/configuration/#base-peth) configuration to `/`, as you can see in [this demo](https://github.com/ProjectEvergreen/greenwood-demo-github-pages). 1. Greenwood [installed and setup](/getting-started/) in your repository, ex. ```shell src/ @@ -39,7 +40,7 @@ With the above in place, let's set everything up! } ``` 1. Create a file called _.github/workflows/gh-pages.yml_ in the repo -1. Now add this GitHub Action, making sure to use the correct branch name for your project; _master_, _main_, etc. (We're leveraging [this action](https://github.com/marketplace/actions/github-pages-action) at the end for the actual auto deploy.) +1. Now add this GitHub Action, _making sure to use the correct branch name for your project_; **_master_, _main_**, etc. (We're leveraging [this action](https://github.com/marketplace/actions/github-pages-action) at the end for the actual auto deploy.) ```yml name: Deploy GitHub Pages @@ -51,15 +52,17 @@ With the above in place, let's set everything up! jobs: build-and-deploy: + runs-on: ubuntu-20.04 + steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.15.0 - name: Install Dependencies run: | - npm ci # or yarn install if using Yarn + npm ci # or replace with yarn, pnpm, etc - name: Build run: | diff --git a/www/templates/page.html b/www/templates/page.html index 404c569ea..cf6ebb705 100644 --- a/www/templates/page.html +++ b/www/templates/page.html @@ -15,7 +15,7 @@ const params = path.split('/').filter(param => param !== ""); const filePath = params.length <= 1 ? `${params[0]}/index.md` : `${params.join("/")}.md` - document.getElementsByTagName('app-shelf')[0].setAttribute('page', params[0]); + document.getElementsByTagName('app-shelf')[0].setAttribute('page', params[1]); editButton.href = `https://github.com/ProjectEvergreen/greenwood/tree/master/www/pages/${filePath}` } From 9f087a36991a15c3024297be13b70c8d171870c5 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 3 Nov 2023 20:29:46 -0400 Subject: [PATCH 29/35] add support for optimizing CSS `:has` / `:is` pseudo-selectors (#1107) * add support for optimizing CSS :has pseudo-selector * add support for :is --- packages/cli/src/plugins/resource/plugin-standard-css.js | 4 ++++ .../fixtures/expected.css | 6 +++++- .../build.config.optimization-default/src/styles/main.css | 8 ++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/plugins/resource/plugin-standard-css.js b/packages/cli/src/plugins/resource/plugin-standard-css.js index 22b3801f4..d52c723e2 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-css.js +++ b/packages/cli/src/plugins/resource/plugin-standard-css.js @@ -46,6 +46,8 @@ function bundleCss(body, url, projectDirectory) { switch (name) { + case 'is': + case 'has': case 'lang': case 'not': case 'nth-child': @@ -153,6 +155,8 @@ function bundleCss(body, url, projectDirectory) { case 'PseudoClassSelector': switch (node.name) { + case 'is': + case 'has': case 'lang': case 'not': case 'nth-child': diff --git a/packages/cli/test/cases/build.config.optimization-default/fixtures/expected.css b/packages/cli/test/cases/build.config.optimization-default/fixtures/expected.css index 5c565df54..5b1bb6add 100644 --- a/packages/cli/test/cases/build.config.optimization-default/fixtures/expected.css +++ b/packages/cli/test/cases/build.config.optimization-default/fixtures/expected.css @@ -54,4 +54,8 @@ a[href*='greenwood'],a[href$='.pdf']{color:orange} @page {size:8.5in 9in;margin-top:4in;} -@font-feature-values Font One{@styleset {nice-style:12}} \ No newline at end of file +@font-feature-values Font One{@styleset {nice-style:12}} + +h1:has(+h2){margin:0 0 0.25rem 0} + +:is(ol,ul,menu:unsupported) :is(ol,ul){color:green} \ No newline at end of file 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 index 2689da7c7..d2836cee1 100644 --- 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 @@ -121,4 +121,12 @@ a[href*="greenwood"], a[href$=".pdf"] { @styleset { nice-style: 12; } +} + +h1:has(+ h2) { + margin: 0 0 0.25rem 0; +} + +:is(ol, ul, menu:unsupported) :is(ol, ul) { + color: green; } \ No newline at end of file From b09a7fe2f4aa2c97b799fe5d9b5be695de3967ed Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 3 Nov 2023 20:51:07 -0400 Subject: [PATCH 30/35] remove leftover base path demo code (#1179) --- www/templates/page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/templates/page.html b/www/templates/page.html index cf6ebb705..404c569ea 100644 --- a/www/templates/page.html +++ b/www/templates/page.html @@ -15,7 +15,7 @@ const params = path.split('/').filter(param => param !== ""); const filePath = params.length <= 1 ? `${params[0]}/index.md` : `${params.join("/")}.md` - document.getElementsByTagName('app-shelf')[0].setAttribute('page', params[1]); + document.getElementsByTagName('app-shelf')[0].setAttribute('page', params[0]); editButton.href = `https://github.com/ProjectEvergreen/greenwood/tree/master/www/pages/${filePath}` } From 26938f36e11c3f9ded032295d79ee6a216f8f8ad Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 4 Nov 2023 09:11:49 -0400 Subject: [PATCH 31/35] v0.29.0-alpha.6 --- lerna.json | 2 +- packages/cli/package.json | 2 +- packages/init/package.json | 2 +- packages/plugin-adapter-netlify/package.json | 4 ++-- packages/plugin-adapter-vercel/package.json | 4 ++-- packages/plugin-babel/package.json | 4 ++-- packages/plugin-google-analytics/package.json | 4 ++-- packages/plugin-graphql/package.json | 4 ++-- packages/plugin-import-commonjs/package.json | 4 ++-- packages/plugin-import-css/package.json | 4 ++-- packages/plugin-import-json/package.json | 4 ++-- packages/plugin-import-jsx/package.json | 4 ++-- packages/plugin-include-html/package.json | 4 ++-- packages/plugin-polyfills/package.json | 4 ++-- packages/plugin-postcss/package.json | 4 ++-- packages/plugin-renderer-lit/package.json | 4 ++-- packages/plugin-renderer-puppeteer/package.json | 4 ++-- packages/plugin-typescript/package.json | 4 ++-- www/package.json | 2 +- 19 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lerna.json b/lerna.json index 7fa3e782d..b1cd7d2e6 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "packages": [ "packages/*", "www" diff --git a/packages/cli/package.json b/packages/cli/package.json index 3bd29862c..5c36fa21a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/cli", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "Greenwood CLI.", "type": "module", "repository": "https://github.com/ProjectEvergreen/greenwood", diff --git a/packages/init/package.json b/packages/init/package.json index 1de028381..5579d7657 100644 --- a/packages/init/package.json +++ b/packages/init/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/init", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A package for scaffolding a new Greenwood project.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/init", diff --git a/packages/plugin-adapter-netlify/package.json b/packages/plugin-adapter-netlify/package.json index 6ccd34069..5acbeae34 100644 --- a/packages/plugin-adapter-netlify/package.json +++ b/packages/plugin-adapter-netlify/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-adapter-netlify", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin for supporting Netlify serverless and edge runtimes.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-adapter-netlify", @@ -31,7 +31,7 @@ "zip-a-folder": "^2.0.0" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5", + "@greenwood/cli": "^0.29.0-alpha.6", "extract-zip": "^2.0.1" } } diff --git a/packages/plugin-adapter-vercel/package.json b/packages/plugin-adapter-vercel/package.json index 24c5e21ad..133c0c2e9 100644 --- a/packages/plugin-adapter-vercel/package.json +++ b/packages/plugin-adapter-vercel/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-adapter-vercel", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin for supporting Vercel serverless and edge runtimes.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-adapter-vercel", @@ -27,6 +27,6 @@ "@greenwood/cli": "^0.28.0" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-babel/package.json b/packages/plugin-babel/package.json index 91e102adc..6abd46fbd 100644 --- a/packages/plugin-babel/package.json +++ b/packages/plugin-babel/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-babel", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin for using Babel and applying it to your JavaScript.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-babel", @@ -36,6 +36,6 @@ "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-private-methods": "^7.10.4", "@babel/runtime": "^7.10.4", - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-google-analytics/package.json b/packages/plugin-google-analytics/package.json index be5eb4c43..d7c4ab9aa 100644 --- a/packages/plugin-google-analytics/package.json +++ b/packages/plugin-google-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-google-analytics", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin adding support for Google Analytics JavaScript tracker.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-google-analytics", @@ -26,6 +26,6 @@ "@greenwood/cli": "^0.4.0" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-graphql/package.json b/packages/plugin-graphql/package.json index 628d911ce..0d06a20b2 100644 --- a/packages/plugin-graphql/package.json +++ b/packages/plugin-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-graphql", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A plugin for using GraphQL for querying your content.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-graphql", @@ -33,6 +33,6 @@ "graphql-tag": "^2.10.1" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-import-commonjs/package.json b/packages/plugin-import-commonjs/package.json index aa2252b3a..70efef84b 100644 --- a/packages/plugin-import-commonjs/package.json +++ b/packages/plugin-import-commonjs/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-import-commonjs", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A plugin for loading CommonJS based modules in the browser using ESM (import / export) syntax.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-commonjs", @@ -31,7 +31,7 @@ "cjs-module-lexer": "^1.0.0" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5", + "@greenwood/cli": "^0.29.0-alpha.6", "lodash": "^4.17.20" } } diff --git a/packages/plugin-import-css/package.json b/packages/plugin-import-css/package.json index 4bd904691..c3e60ab6d 100644 --- a/packages/plugin-import-css/package.json +++ b/packages/plugin-import-css/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-import-css", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin to allow you to use ESM (import) syntax to load your CSS.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-css", @@ -27,6 +27,6 @@ "@greenwood/cli": "^0.4.0" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-import-json/package.json b/packages/plugin-import-json/package.json index db768c7d6..fe822ac2d 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.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin to allow you to use ESM (import) syntax to load your JSON.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-json", @@ -27,6 +27,6 @@ "@greenwood/cli": "^0.12.3" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-import-jsx/package.json b/packages/plugin-import-jsx/package.json index ada2a9e06..1de661d33 100644 --- a/packages/plugin-import-jsx/package.json +++ b/packages/plugin-import-jsx/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-import-jsx", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin to write JSX rendering Web Components compatible with WCC.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-jsx", @@ -30,6 +30,6 @@ "wc-compiler": "~0.9.0" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-include-html/package.json b/packages/plugin-include-html/package.json index 1da19e31c..36e2de22f 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.29.0-alpha.5", + "version": "0.29.0-alpha.6", "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", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-include-html", @@ -27,6 +27,6 @@ "@greenwood/cli": "^0.4.0" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-polyfills/package.json b/packages/plugin-polyfills/package.json index cbcd82e73..68b2d122d 100644 --- a/packages/plugin-polyfills/package.json +++ b/packages/plugin-polyfills/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-polyfills", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin adding support for Web Component related polyfills like Custom Elements and Shadow DOM.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-polyfills", @@ -29,6 +29,6 @@ "@webcomponents/webcomponentsjs": "^2.6.0" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-postcss/package.json b/packages/plugin-postcss/package.json index 3bdff01f7..b8649c81d 100644 --- a/packages/plugin-postcss/package.json +++ b/packages/plugin-postcss/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-postcss", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin for loading PostCSS configuration and applying it to your CSS.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss", @@ -31,6 +31,6 @@ "postcss-preset-env": "^7.0.1" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-renderer-lit/package.json b/packages/plugin-renderer-lit/package.json index e0e35d3c0..85c631ee6 100644 --- a/packages/plugin-renderer-lit/package.json +++ b/packages/plugin-renderer-lit/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-renderer-lit", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A server-side rendering plugin for Lit based Greenwood projects.", "type": "module", "repository": "https://github.com/ProjectEvergreen/greenwood", @@ -31,7 +31,7 @@ "@lit-labs/ssr": "^2.0.1" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5", + "@greenwood/cli": "^0.29.0-alpha.6", "lit": "^2.1.1" } } diff --git a/packages/plugin-renderer-puppeteer/package.json b/packages/plugin-renderer-puppeteer/package.json index 8c4933be3..7ada08655 100644 --- a/packages/plugin-renderer-puppeteer/package.json +++ b/packages/plugin-renderer-puppeteer/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-renderer-puppeteer", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin to allow headless browser rendering with Puppeteer.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-renderer-puppeteer", @@ -30,6 +30,6 @@ "puppeteer": "^15.3.2" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/packages/plugin-typescript/package.json b/packages/plugin-typescript/package.json index 761f6e1dc..5905bc9a3 100644 --- a/packages/plugin-typescript/package.json +++ b/packages/plugin-typescript/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/plugin-typescript", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "description": "A Greenwood plugin for writing TypeScript.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-typescript", @@ -29,6 +29,6 @@ "typescript": "^5.1.6" }, "devDependencies": { - "@greenwood/cli": "^0.29.0-alpha.5" + "@greenwood/cli": "^0.29.0-alpha.6" } } diff --git a/www/package.json b/www/package.json index 847ed1cc3..6531c5e5c 100644 --- a/www/package.json +++ b/www/package.json @@ -1,6 +1,6 @@ { "name": "@greenwood/www", - "version": "0.29.0-alpha.5", + "version": "0.29.0-alpha.6", "private": true, "type": "module", "description": "Greenwood website workspace.", From ce0c14f0c8476f82a3c54f8739371288192ec74c Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Tue, 7 Nov 2023 08:18:44 -0500 Subject: [PATCH 32/35] ensure resolving graph.json respects base path configuration (#1180) --- .../plugins/resource/plugin-standard-json.js | 6 +++-- .../develop.config.base-path.spec.js | 25 +++++++++++++++++++ .../develop.default/develop.default.spec.js | 25 +++++++++++++++++++ .../serve.config.base-path.spec.js | 25 +++++++++++++++++++ .../cases/serve.default/serve.default.spec.js | 24 ++++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/plugins/resource/plugin-standard-json.js b/packages/cli/src/plugins/resource/plugin-standard-json.js index ee29ae8be..726c9b5c1 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-json.js +++ b/packages/cli/src/plugins/resource/plugin-standard-json.js @@ -17,8 +17,9 @@ class StandardJsonResource extends ResourceInterface { async shouldServe(url) { const { protocol, pathname } = url; + const { basePath } = this.compilation.config; const isJson = pathname.split('.').pop() === this.extensions[0]; - const isGraphJson = pathname === '/graph.json'; + const isGraphJson = pathname === `${basePath}/graph.json`; const isWorkspaceFile = protocol === 'file:' && await checkResourceExists(url); return isJson && (isWorkspaceFile || isGraphJson); @@ -27,7 +28,8 @@ class StandardJsonResource extends ResourceInterface { async serve(url) { const { pathname } = url; const { scratchDir } = this.compilation.context; - const finalUrl = pathname.startsWith('/graph.json') + const { basePath } = this.compilation.config; + const finalUrl = pathname === `${basePath}/graph.json` ? new URL('./graph.json', scratchDir) : url; const contents = await fs.readFile(finalUrl, 'utf-8'); diff --git a/packages/cli/test/cases/develop.config.base-path/develop.config.base-path.spec.js b/packages/cli/test/cases/develop.config.base-path/develop.config.base-path.spec.js index 44315f575..e70c56a98 100644 --- a/packages/cli/test/cases/develop.config.base-path/develop.config.base-path.spec.js +++ b/packages/cli/test/cases/develop.config.base-path/develop.config.base-path.spec.js @@ -287,6 +287,31 @@ describe('Develop Greenwood With: ', function() { expect(cards.length).to.be.greaterThan(0); }); }); + + describe('Fetching graph.json client side', function() { + let response; + let graph; + + before(async function() { + response = await fetch(`${hostname}:${port}${basePath}/graph.json`); + graph = await response.clone().json(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.contain('application/json'); + done(); + }); + + it('should return a 200', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should have the expected length for all content', function(done) { + expect(graph.length).to.equal(3); + done(); + }); + }); }); after(function() { diff --git a/packages/cli/test/cases/develop.default/develop.default.spec.js b/packages/cli/test/cases/develop.default/develop.default.spec.js index e31d2466e..602b1e08a 100644 --- a/packages/cli/test/cases/develop.default/develop.default.spec.js +++ b/packages/cli/test/cases/develop.default/develop.default.spec.js @@ -1552,6 +1552,31 @@ describe('Develop Greenwood With: ', function() { done(); }); }); + + describe('Fetching graph.json client side', function() { + let response; + let graph; + + before(async function() { + response = await fetch(`${hostname}:${port}/graph.json`); + graph = await response.clone().json(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.contain('application/json'); + done(); + }); + + it('should return a 200', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should have the expected length for all content', function(done) { + expect(graph.length).to.equal(2); + done(); + }); + }); }); after(function() { diff --git a/packages/cli/test/cases/serve.config.base-path/serve.config.base-path.spec.js b/packages/cli/test/cases/serve.config.base-path/serve.config.base-path.spec.js index 267c476cc..62c3f3f60 100644 --- a/packages/cli/test/cases/serve.config.base-path/serve.config.base-path.spec.js +++ b/packages/cli/test/cases/serve.config.base-path/serve.config.base-path.spec.js @@ -437,6 +437,31 @@ describe('Serve Greenwood With: ', function() { expect(cards.length).to.be.greaterThan(0); }); }); + + describe('Fetching graph.json client side', function() { + let response; + let graph; + + before(async function() { + response = await fetch(`${hostname}${basePath}/graph.json`); + graph = await response.clone().json(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.contain('application/json'); + done(); + }); + + it('should return a 200', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should have the expected length for all content', function(done) { + expect(graph.length).to.equal(4); + done(); + }); + }); }); after(function() { diff --git a/packages/cli/test/cases/serve.default/serve.default.spec.js b/packages/cli/test/cases/serve.default/serve.default.spec.js index 265576e8b..d30ace017 100644 --- a/packages/cli/test/cases/serve.default/serve.default.spec.js +++ b/packages/cli/test/cases/serve.default/serve.default.spec.js @@ -456,6 +456,30 @@ describe('Serve Greenwood With: ', function() { }); }); + describe('Fetching graph.json client side', function() { + let response; + let graph; + + before(async function() { + response = await fetch(`${hostname}/graph.json`); + graph = await response.clone().json(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.contain('application/json'); + done(); + }); + + it('should return a 200', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should have the expected length for all content', function(done) { + expect(graph.length).to.equal(2); + done(); + }); + }); }); after(function() { From 7c9c23f36640d5f07b891c2626a8a9c1efd9bec0 Mon Sep 17 00:00:00 2001 From: Paul Barry Date: Wed, 8 Nov 2023 08:55:36 -0500 Subject: [PATCH 33/35] chore/Issue 1016: Replace the use of request with fetch in test cases (#1172) * Issue 1016: Initital attempt at using fetch over request * Issue-1016: Refactor IP to use localhost url * Issue-1016: Initial POST refactor to fetch * Issue-1016: Progress on removal of request * Issue-1016: Additional removal of request * Issue-1016: Finalize plugin tests * use response.text() * refactor init spec to fetch * all specs refactored to use fetch * refactoring all specs --------- Co-authored-by: Owen Buckley --- .../develop.default.hud-disabled.spec.js | 36 +- .../develop.default.hud.spec.js | 36 +- .../develop.default/develop.default.spec.js | 622 +++++------------- .../develop.default/src/api/submit-json.js | 4 +- .../develop.plugins.context.spec.js | 69 +- .../cases/develop.spa/develop.spa.spec.js | 125 +--- .../cases/develop.ssr/develop.ssr.spec.js | 48 +- .../serve.config.static-router.spec.js | 24 +- .../serve.default.api.spec.js | 168 ++--- ...e.default.ssr-prerender-api-hybrid.spec.js | 45 +- .../serve.default.ssr-prerender.spec.js | 24 +- .../serve.default.ssr-static-export.spec.js | 24 +- .../serve.default.ssr.spec.js | 184 ++---- .../cases/serve.default/serve.default.spec.js | 244 ++----- .../test/cases/serve.spa/serve.spa.spec.js | 75 +-- .../theme-pack/theme-pack.develop.spec.js | 69 +- .../develop.default/develop.default.spec.js | 25 +- .../cases/build.default/build.default.spec.js | 15 +- .../cases/build.default/build.default.spec.js | 29 +- .../develop.default/develop.default.spec.js | 97 +-- .../qraphql-server/graphql-server.spec.js | 68 +- .../develop.default/develop.default.spec.js | 67 +- .../cases/exp-serve.ssr/exp-serve.ssr.spec.js | 66 +- .../develop.default/develop.default.spec.js | 33 +- .../cases/exp-serve.ssr/exp-serve.ssr.spec.js | 73 +- .../cases/serve.default/serve.default.spec.js | 49 +- .../develop.default/develop.default.spec.js | 33 +- .../exp-prerender.serve.ssr.spec.js | 26 +- .../cases/exp-serve.ssr/exp-serve.ssr.spec.js | 38 +- 29 files changed, 699 insertions(+), 1717 deletions(-) diff --git a/packages/cli/test/cases/develop.default.hud-disabled/develop.default.hud-disabled.spec.js b/packages/cli/test/cases/develop.default.hud-disabled/develop.default.hud-disabled.spec.js index f05504d7f..41f5801b0 100644 --- a/packages/cli/test/cases/develop.default.hud-disabled/develop.default.hud-disabled.spec.js +++ b/packages/cli/test/cases/develop.default.hud-disabled/develop.default.hud-disabled.spec.js @@ -20,7 +20,6 @@ import fs from 'fs'; import { JSDOM } from 'jsdom'; import path from 'path'; import { getSetupFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { Runner } from 'gallinago'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { fileURLToPath, URL } from 'url'; @@ -61,46 +60,31 @@ describe('Develop Greenwood With: ', function() { describe('Develop command specific HUD HTML behaviors when disabled', function() { let response = {}; let sourceHtml = ''; - let dom; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}`, - headers: { - accept: 'text/html' - } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - - dom = new JSDOM(body); - sourceHtml = fs.readFileSync(fileURLToPath(new URL('./src/pages/index.html', import.meta.url)), 'utf-8'); - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}`); + body = await response.clone().text(); + sourceHtml = fs.readFileSync(fileURLToPath(new URL('./src/pages/index.html', import.meta.url)), 'utf-8'); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should contain the appropriate HUD output in the response', function(done) { - const body = dom.window.document.querySelectorAll('body')[0]; + const dom = new JSDOM(body); + const bodyTag = dom.window.document.querySelectorAll('body')[0]; - expect(body.textContent).not.to.contain('Malformed HTML detected, please check your closing tags or an HTML formatter'); - expect(body.textContent.replace(/\\n/g, '').trim()).not.to.contain(sourceHtml.replace(/\\n/g, '').trim()); + expect(bodyTag.textContent).not.to.contain('Malformed HTML detected, please check your closing tags or an HTML formatter'); + expect(bodyTag.textContent.replace(/\\n/g, '').trim()).not.to.contain(sourceHtml.replace(/\\n/g, '').trim()); done(); }); diff --git a/packages/cli/test/cases/develop.default.hud/develop.default.hud.spec.js b/packages/cli/test/cases/develop.default.hud/develop.default.hud.spec.js index df5768c6b..4d0d00b04 100644 --- a/packages/cli/test/cases/develop.default.hud/develop.default.hud.spec.js +++ b/packages/cli/test/cases/develop.default.hud/develop.default.hud.spec.js @@ -20,7 +20,6 @@ import fs from 'fs'; import { JSDOM } from 'jsdom'; import path from 'path'; import { getSetupFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { Runner } from 'gallinago'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { fileURLToPath, URL } from 'url'; @@ -61,46 +60,31 @@ describe('Develop Greenwood With: ', function() { describe('Develop command specific HUD HTML behaviors', function() { let response = {}; let sourceHtml = ''; - let dom; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}`, - headers: { - accept: 'text/html' - } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - - dom = new JSDOM(body); - sourceHtml = fs.readFileSync(fileURLToPath(new URL('./src/pages/index.html', import.meta.url)), 'utf-8'); - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}`); + body = await response.clone().text(); + sourceHtml = fs.readFileSync(fileURLToPath(new URL('./src/pages/index.html', import.meta.url)), 'utf-8'); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should contain the appropriate HUD output in the response', function(done) { - const body = dom.window.document.querySelectorAll('body')[0]; + const dom = new JSDOM(body); + const bodyTag = dom.window.document.querySelectorAll('body')[0]; - expect(body.textContent).to.contain('Malformed HTML detected, please check your closing tags or an HTML formatter'); - expect(body.textContent.replace(/\\n/g, '').trim()).to.contain(sourceHtml.replace(/\\n/g, '').trim()); + expect(bodyTag.textContent).to.contain('Malformed HTML detected, please check your closing tags or an HTML formatter'); + expect(bodyTag.textContent.replace(/\\n/g, '').trim()).to.contain(sourceHtml.replace(/\\n/g, '').trim()); done(); }); diff --git a/packages/cli/test/cases/develop.default/develop.default.spec.js b/packages/cli/test/cases/develop.default/develop.default.spec.js index 602b1e08a..b6fd01a72 100644 --- a/packages/cli/test/cases/develop.default/develop.default.spec.js +++ b/packages/cli/test/cases/develop.default/develop.default.spec.js @@ -48,7 +48,6 @@ import fs from 'fs'; import { JSDOM } from 'jsdom'; import path from 'path'; import { getDependencyFiles, getSetupFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -464,34 +463,19 @@ describe('Develop Greenwood With: ', function() { let expectedImportMap; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}`, - headers: { - accept: 'text/html' - } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - - dom = new JSDOM(body); - expectedImportMap = JSON.parse(fs.readFileSync(new URL('./import-map.snapshot.json', import.meta.url), 'utf-8')); - - resolve(); - }); - }); + response = await fetch(`http://127.0.0.1:${port}`); + const data = await response.text(); + dom = new JSDOM(data); + expectedImportMap = JSON.parse(fs.readFileSync(new URL('./import-map.snapshot.json', import.meta.url), 'utf-8')); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); @@ -570,32 +554,18 @@ describe('Develop Greenwood With: ', function() { let dom; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/404/`, - headers: { - accept: 'text/html' - } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - - dom = new JSDOM(body); - resolve(); - }); - }); + response = await fetch(`http://127.0.0.1:${port}/404/`); + const data = await response.text(); + dom = new JSDOM(data); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); @@ -619,68 +589,50 @@ describe('Develop Greenwood With: ', function() { describe('Develop command specific JavaScript behaviors for user authored custom element', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/components/header.js`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/components/header.js`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers.get('content-type')).to.equal('text/javascript'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('class HeaderComponent extends HTMLElement'); + expect(body).to.contain('class HeaderComponent extends HTMLElement'); done(); }); }); describe('Develop command specific CSS behaviors', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/styles/main.css`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/styles/main.css`); + body = await response.clone().text(); }); - it('should eturn a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + it('should return a 200 status', function(done) { + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers.get('content-type')).to.equal('text/css'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('color: blue;'); + expect(body).to.contain('color: blue;'); done(); }); }); @@ -688,137 +640,100 @@ describe('Develop Greenwood With: ', function() { describe('Develop command with image (png) specific behavior', function() { const ext = 'png'; let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - - request.get(`${hostname}:${port}/assets/logo.${ext}`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/assets/logo.${ext}`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal(`image/${ext}`); + expect(response.headers.get('content-type')).to.equal(`image/${ext}`); done(); }); it('should return binary data', function(done) { - expect(response.body).to.contain('PNG'); + expect(body).to.contain('PNG'); done(); }); }); describe('Develop command with image (ico) specific behavior', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/assets/favicon.ico`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(response); - }); - }); + response = await fetch(`${hostname}:${port}/assets/favicon.ico`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('image/x-icon'); + expect(response.headers.get('content-type')).to.equal('image/x-icon'); done(); }); it('should return binary data', function(done) { - expect(response.body).to.contain('\u0000'); + expect(body).to.contain('\u0000'); done(); }); }); describe('Develop command with image (webp) specific behavior', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/assets/river-valley.webp`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(response); - }); - }); + response = await fetch(`${hostname}:${port}/assets/river-valley.webp`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('image/webp'); + expect(response.headers.get('content-type')).to.equal('image/webp'); done(); }); it('should return binary data', function(done) { - expect(response.body).to.contain('\u0000'); + expect(body).to.contain('\u0000'); done(); }); }); describe('Develop command with image (avif) specific behavior', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/assets/fox.avif`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(response); - }); - }); + response = await fetch(`${hostname}:${port}/assets/fox.avif`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('image/avif'); + expect(response.headers.get('content-type')).to.equal('image/avif'); done(); }); it('should return binary data', function(done) { - expect(response.body).to.contain('\u0000'); + expect(body).to.contain('\u0000'); done(); }); }); @@ -826,34 +741,25 @@ describe('Develop Greenwood With: ', function() { describe('Develop command with image (svg) specific behavior', function() { const ext = 'svg'; let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/assets/webcomponents.${ext}`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/assets/webcomponents.${ext}`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal(`image/${ext}+xml`); + expect(response.headers.get('content-type')).to.equal(`image/${ext}+xml`); done(); }); it('should return the correct response body', function(done) { - expect(response.body.indexOf(' { - request.get(`${hostname}:${port}/assets/source-sans-pro.woff?v=1`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/assets/source-sans-pro.${ext}?v=1`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal(`font/${ext}`); + expect(response.headers.get('content-type')).to.equal(`font/${ext}`); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('wOFF'); + expect(body).to.contain('wOFF'); done(); }); }); @@ -896,262 +793,190 @@ describe('Develop Greenwood With: ', function() { describe('Develop command with generic video container format (.mp4) behavior', function() { const ext = 'mp4'; let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `${hostname}:${port}/assets/splash-clip.mp4` - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/assets/splash-clip.mp4`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type header', function(done) { - expect(response.headers['content-type']).to.equal(`video/${ext}`); + expect(response.headers.get('content-type')).to.equal(`video/${ext}`); done(); }); it('should return the correct content length header', function(done) { - expect(response.headers['content-length']).to.equal('2498461'); + expect(response.headers.get('content-length')).to.equal('2498461'); done(); }); it('should return the correct etag header', function(done) { - expect(response.headers.etag).to.equal('2130309740'); + expect(response.headers.get('etag')).to.equal('2130309740'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain(ext); + expect(body).to.contain(ext); done(); }); }); describe('Develop command with generic video container format (.mp4) behavior that should return an etag hit', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `${hostname}:${port}/assets/splash-clip.mp4`, - headers: { - 'if-none-match': '2130309740' - } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/assets/splash-clip.mp4`, { headers: new Headers({ 'if-none-match': '2130309740' }) }); + body = await response.clone().text(); }); it('should return a 304 status', function(done) { - expect(response.statusCode).to.equal(304); + expect(response.status).to.equal(304); done(); }); it('should return an empty body', function(done) { - expect(response.body).to.contain(''); + expect(body).to.contain(''); done(); }); it('should return the correct cache-control header', function(done) { - expect(response.headers['cache-control']).to.equal('no-cache'); + expect(response.headers.get('cache-control')).to.equal('no-cache'); done(); }); }); describe('Develop command with audio format (.mp3) behavior', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/assets/song-sample.mp3`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/assets/song-sample.mp3`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('audio/mpeg'); + expect(response.headers.get('content-type')).to.equal('audio/mpeg'); done(); }); it('should return the correct content length', function(done) { - expect(response.headers['content-length']).to.equal('5425061'); + expect(response.headers.get('content-length')).to.equal('5425061'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('ID3'); + expect(body).to.contain('ID3'); done(); }); }); describe('Develop command with JSON specific behavior', function() { let response = {}; + let data; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/assets/data.json`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = JSON.parse(body); - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/assets/data.json`); + data = await response.clone().json(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('application/json'); + expect(response.headers.get('content-type')).to.equal('application/json'); done(); }); it('should return the correct response body', function(done) { - expect(response.body.name).to.equal('Marvin'); + expect(data.name).to.equal('Marvin'); done(); }); }); describe('Develop command with source map specific behavior', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/node_modules/lit-html/lit-html.js.map`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/node_modules/lit-html/lit-html.js.map`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('application/json'); + expect(response.headers.get('content-type')).to.equal('application/json'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('"sources":["src/lit-html.ts"]'); + expect(body).to.contain('"sources":["src/lit-html.ts"]'); done(); }); }); describe('Develop command specific node modules resolution behavior for JS with query string', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/node_modules/lit-html/lit-html.js?type=xyz`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/node_modules/lit-html/lit-html.js?type=xyz`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers.get('content-type')).to.equal('text/javascript'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('Copyright 2017 Google LLC'); + expect(body).to.contain('Copyright 2017 Google LLC'); done(); }); }); describe('Develop command specific node modules resolution behavior for CSS with query string', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/node_modules/simpledotcss/simple.css?xyz=123` - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`http://127.0.0.1:${port}/node_modules/simpledotcss/simple.css?xyz=123`); + body = await response.clone().text(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers.get('content-type')).to.equal('text/css'); done(); }); it('should correctly return CSS from the developers local files', function(done) { - expect(response.body).to.contain('/* Set the global variables for everything. Change these to use your own fonts/colours. */'); + expect(body).to.contain('/* Set the global variables for everything. Change these to use your own fonts/colours. */'); done(); }); }); @@ -1160,34 +985,25 @@ describe('Develop Greenwood With: ', function() { // https://github.com/ProjectEvergreen/greenwood/pull/687 describe('Develop command specific workspace resolution when local file matches a file also in node_modules', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/lit-html.js`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/lit-html.js`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers.get('content-type')).to.equal('text/javascript'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.equal('console.debug(\'its just a prank bro!\');'); + expect(body).to.equal('console.debug(\'its just a prank bro!\');'); done(); }); }); @@ -1195,34 +1011,25 @@ describe('Develop Greenwood With: ', function() { // https://github.com/ProjectEvergreen/greenwood/issues/715 describe('Develop command node_modules resolution for a transient dependency\'s own imports', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/node_modules/@bundled-es-modules/message-format/MessageFormat.js`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/node_modules/@bundled-es-modules/message-format/MessageFormat.js`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers.get('content-type')).to.equal('text/javascript'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('export default messageFormat;'); + expect(body).to.contain('export default messageFormat;'); done(); }); }); @@ -1231,34 +1038,25 @@ describe('Develop Greenwood With: ', function() { // @lion/calendar/define -> /node_modules/@lion/calendar/lion-calendar.js describe('Develop command node_modules resolution for a flat export map entry from a dependency (not import or default)', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/node_modules/@lion/calendar/lion-calendar.js`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/node_modules/@lion/calendar/lion-calendar.js`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers.get('content-type')).to.equal('text/javascript'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('customElements.define(\'lion-calendar\', LionCalendar);'); + expect(body).to.contain('customElements.define(\'lion-calendar\', LionCalendar);'); done(); }); }); @@ -1266,39 +1064,30 @@ describe('Develop Greenwood With: ', function() { // need some better 404 handling here (promise reject handling for assets and routes) describe('Develop command with default 404 behavior', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/abc.js`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/abc.js`); + body = await response.clone().text(); }); it('should return a 404 status', function(done) { - expect(response.statusCode).to.equal(404); + expect(response.status).to.equal(404); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/plain; charset=utf-8'); + expect(response.headers.get('content-type')).to.equal('text/plain; charset=utf-8'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain(''); + expect(body).to.contain(''); done(); }); it('should return the correct status message body', function(done) { - expect(response.statusMessage).to.contain('Not Found'); + expect(response.statusText).to.contain('Not Found'); done(); }); }); @@ -1306,29 +1095,20 @@ describe('Develop Greenwood With: ', function() { // proxies to https://jsonplaceholder.typicode.com/posts via greenwood.config.js describe('Develop command with dev proxy', function() { let response = {}; + let data; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/posts?id=7`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = JSON.parse(body); - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/posts?id=7`); + data = await response.clone().json(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('application/json; charset=utf-8'); + expect(response.headers.get('content-type')).to.equal('application/json; charset=utf-8'); done(); }); @@ -1339,7 +1119,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct response body', function(done) { - expect(response.body).to.have.lengthOf(1); + expect(data).to.have.lengthOf(1); done(); }); }); @@ -1351,7 +1131,6 @@ describe('Develop Greenwood With: ', function() { before(async function() { response = await fetch(`${hostname}:${port}/api/greeting?name=${name}`); - data = await response.json(); }); @@ -1381,66 +1160,50 @@ describe('Develop Greenwood With: ', function() { describe('Develop command with API specific behaviors for an HTML ("fragment") API', function() { const name = 'Greenwood'; let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/api/fragment?name=${name}`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/api/fragment?name=${name}`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return a custom status message', function(done) { - expect(response.statusMessage).to.equal('SUCCESS!!!'); + expect(response.statusText).to.equal('SUCCESS!!!'); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain(`

Hello ${name}!!!

`); + expect(body).to.contain(`

Hello ${name}!!!

`); done(); }); }); describe('Develop command with API specific behaviors with a custom response', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/api/missing`, (err, res) => { - if (err) { - reject(); - } - - response = res; - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/api/missing`); + body = await response.clone().text(); }); it('should return a 404 status', function(done) { - expect(response.statusCode).to.equal(404); + expect(response.status).to.equal(404); done(); }); it('should return a body of not found', function(done) { - expect(response.body).to.equal('Not Found'); + expect(body).to.equal('Not Found'); done(); }); }); @@ -1449,20 +1212,11 @@ describe('Develop Greenwood With: ', function() { let response = {}; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}:${port}/api/nothing`, (err, res) => { - if (err) { - reject(); - } - - response = res; - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/api/nothing`); }); it('should return a custom status code', function(done) { - expect(response.statusCode).to.equal(204); + expect(response.status).to.equal(204); done(); }); }); @@ -1470,85 +1224,61 @@ describe('Develop Greenwood With: ', function() { describe('Develop command with POST API specific behaviors for JSON', function() { const param = 'Greenwood'; let response = {}; + let data; before(async function() { - return new Promise((resolve, reject) => { - request.post({ - url: `${hostname}:${port}/api/submit-json`, - json: true, - body: { name: param } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(response); - }); + response = await fetch(`${hostname}:${port}/api/submit-json`, { + method: 'POST', + body: JSON.stringify({ name: param }) }); + data = await response.json(); }); - it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); - done(); + it('should return a 200 status', function() { + expect(response.status).to.equal(200); }); - it('should return the expected response message', function(done) { - const { message } = response.body; - - expect(message).to.equal(`Thank you ${param} for your submission!`); - done(); + it('should return the expected response message', function() { + expect(data.message).to.equal(`Thank you ${param} for your submission!`); }); - it('should return the expected content type header', function(done) { - expect(response.headers['content-type']).to.equal('application/json'); - done(); + it('should return the expected content type header', function() { + expect(response.headers.get('content-type')).to.equal('application/json'); }); - it('should return the secret header in the response', function(done) { - expect(response.headers['x-secret']).to.equal('1234'); - done(); + it('should return the secret header in the response', function() { + expect(response.headers.get('x-secret')).to.equal('1234'); }); }); describe('Develop command with POST API specific behaviors for FormData', function() { const param = 'Greenwood'; let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.post({ - url: `${hostname}:${port}/api/submit-form-data`, - form: { - name: param - } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(response); - }); + response = await fetch(`${hostname}:${port}/api/submit-form-data`, { + method: 'POST', + body: new URLSearchParams({ name: param }).toString(), + headers: new Headers({ + 'content-type': 'application/x-www-form-urlencoded' + }) }); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); - it('should return the expected response message', function(done) { - expect(response.body).to.equal(`Thank you ${param} for your submission!`); + it('should return the expected content type header', function(done) { + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); - it('should return the expected content type header', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + it('should return the expected response message', function(done) { + expect(body).to.equal(`Thank you ${param} for your submission!`); done(); }); }); diff --git a/packages/cli/test/cases/develop.default/src/api/submit-json.js b/packages/cli/test/cases/develop.default/src/api/submit-json.js index b43a3e367..cb8ba972c 100644 --- a/packages/cli/test/cases/develop.default/src/api/submit-json.js +++ b/packages/cli/test/cases/develop.default/src/api/submit-json.js @@ -1,6 +1,6 @@ export async function handler(request) { - const formData = await request.json(); - const { name } = formData; + const data = await request.json(); + const { name } = data; const body = { message: `Thank you ${name} for your submission!` }; return new Response(JSON.stringify(body), { diff --git a/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js b/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js index 54b57579b..438931fce 100644 --- a/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js +++ b/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js @@ -22,7 +22,6 @@ import chai from 'chai'; import fs from 'fs'; import { JSDOM } from 'jsdom'; import path from 'path'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -66,27 +65,13 @@ describe('Develop Greenwood With: ', function() { let dom; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}`, - headers: { - accept: 'text/html' - } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - - dom = new JSDOM(body); - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}`); + const body = await response.clone().text(); + dom = new JSDOM(body); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); @@ -116,37 +101,26 @@ describe('Develop Greenwood With: ', function() { describe('Custom Theme Pack internal logic for resolving theme.css for local development', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/node_modules/${packageJson.name}/dist/styles/theme.css` - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/node_modules/${packageJson.name}/dist/styles/theme.css`); + body = await response.clone().text(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers.get('content-type')).to.equal('text/css'); done(); }); it('should correctly return CSS from the developers local files', function(done) { - expect(response.body).to.equal(':root {\n --color-primary: #135;\n}'); + expect(body).to.equal(':root {\n --color-primary: #135;\n}'); done(); }); @@ -154,37 +128,26 @@ describe('Develop Greenwood With: ', function() { describe('Custom Theme Pack internal logic for resolving greeting.js for local development', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/node_modules/${packageJson.name}/dist/components/greeting.js` - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/node_modules/${packageJson.name}/dist/components/greeting.js`); + body = await response.clone().text(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers.get('content-type')).to.equal('text/javascript'); done(); }); it('should correctly return JavaScript from the developers local files', function(done) { - expect(response.body).to.contain('customElements.define(\'x-greeting\', GreetingComponent);'); + expect(body).to.contain('customElements.define(\'x-greeting\', GreetingComponent);'); done(); }); diff --git a/packages/cli/test/cases/develop.spa/develop.spa.spec.js b/packages/cli/test/cases/develop.spa/develop.spa.spec.js index 6fffefd76..f4b492202 100644 --- a/packages/cli/test/cases/develop.spa/develop.spa.spec.js +++ b/packages/cli/test/cases/develop.spa/develop.spa.spec.js @@ -21,7 +21,6 @@ import chai from 'chai'; import fs from 'fs'; import path from 'path'; import { getDependencyFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -75,117 +74,78 @@ describe('Develop Greenwood With: ', function() { describe('Develop command specific HTML behaviors for client side routing at root - /', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/`, - headers: { - accept: 'text/html' - } - }, (err, res) => { - if (err) { - reject(); - } - - response = res; - - resolve(); - }); - }); + response = await fetch(`${hostname}:${port}/`); + body = await response.clone().text(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the expected body contents', function(done) { - expect(removeWhiteSpace(response.body.match(BODY_REGEX)[0])).to.equal(expected); + expect(removeWhiteSpace(body.match(BODY_REGEX)[0])).to.equal(expected); done(); }); }); describe('Develop command specific HTML behaviors for client side routing at 1 level route - /', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/artists/`, - headers: { - accept: 'text/html' - } - }, (err, res) => { - if (err) { - reject(); - } - - response = res; - - resolve(); - }); - }); + response = await fetch(`http://127.0.0.1:${port}/artists/`); + body = await response.clone().text(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the expected body contents', function(done) { - expect(removeWhiteSpace(response.body.match(BODY_REGEX)[0])).to.equal(expected); + expect(removeWhiteSpace(body.match(BODY_REGEX)[0])).to.equal(expected); done(); }); }); describe('Develop command specific HTML behaviors for client side routing at 1 level route - //:id', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/artists/1`, - headers: { - accept: 'text/html' - } - }, (err, res) => { - if (err) { - reject(); - } - - response = res; - - resolve(); - }); - }); + response = await fetch(`http://127.0.0.1:${port}/artists/1`); + body = await response.clone().text(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the expected body contents', function(done) { - expect(removeWhiteSpace(response.body.match(BODY_REGEX)[0])).to.equal(expected); + expect(removeWhiteSpace(body.match(BODY_REGEX)[0])).to.equal(expected); done(); }); }); @@ -193,36 +153,26 @@ describe('Develop Greenwood With: ', function() { // https://github.com/ProjectEvergreen/greenwood/issues/1064 describe('Develop command specific workspace resolution behavior that does not think its a client side route', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/events/main.css` - }, (err, res) => { - if (err) { - reject(); - } - - response = res; - - resolve(); - }); - }); + response = await fetch(`http://127.0.0.1:${port}/events/main.css`); + body = await response.clone().text(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers.get('content-type')).to.equal('text/css'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the expected body contents', function(done) { - expect(response.body.replace(/\n/g, '').indexOf('* { color: red;}')).to.equal(0); + expect(body.replace(/\n/g, '').indexOf('* { color: red;}')).to.equal(0); done(); }); }); @@ -230,39 +180,30 @@ describe('Develop Greenwood With: ', function() { // https://github.com/ProjectEvergreen/greenwood/issues/803 describe('Develop command specific node modules resolution behavior that does not think its a client side route', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get({ - url: `http://127.0.0.1:${port}/node_modules/simpledotcss/simple.css`, - headers: { - accept: 'ext/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' - } - }, (err, res) => { - if (err) { - reject(); - } - - response = res; - - resolve(); - }); + response = await fetch(`http://127.0.0.1:${port}/node_modules/simpledotcss/simple.css`, { + headers: new Headers({ + accept: 'ext/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' + }) }); + body = await response.clone().text(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers.get('content-type')).to.equal('text/css'); done(); }); it('should return a 200', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the expected body contents', function(done) { - expect(response.body.indexOf('/* Set the global variables for everything. Change these to use your own fonts/colours. */')).to.equal(0); + expect(body.indexOf('/* Set the global variables for everything. Change these to use your own fonts/colours. */')).to.equal(0); done(); }); }); diff --git a/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js index d0fac0374..1f1bf608c 100644 --- a/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js +++ b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js @@ -26,7 +26,6 @@ import fs from 'fs'; import { JSDOM } from 'jsdom'; import path from 'path'; import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -139,39 +138,29 @@ describe('Develop Greenwood With: ', function() { let response = {}; let dom; let artistsPageGraphData; + let body; before(async function() { const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, '.greenwood/graph.json'), 'utf-8')); + response = await fetch(`${hostname}/artists/`); + body = await response.text(); + dom = new JSDOM(body); artistsPageGraphData = graph.filter(page => page.route === '/artists/')[0]; - - return new Promise((resolve, reject) => { - request.get(`${hostname}/artists/`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - dom = new JSDOM(body); - - resolve(); - }); - }); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a response body', function(done) { - expect(response.body).to.not.be.undefined; + expect(body).to.not.be.undefined; done(); }); @@ -241,35 +230,26 @@ describe('Develop Greenwood With: ', function() { const postId = 1; let response = {}; let dom = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/post/?id=${postId}`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - dom = new JSDOM(body); - - resolve(); - }); - }); + response = await fetch(`${hostname}/post/?id=${postId}`); + body = await response.clone().text(); + dom = new JSDOM(body); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a response body', function(done) { - expect(response.body).to.not.be.undefined; + expect(body).to.not.be.undefined; done(); }); diff --git a/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js b/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js index e990e335d..d3be8b253 100644 --- a/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js +++ b/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js @@ -24,7 +24,6 @@ import chai from 'chai'; import path from 'path'; import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -73,35 +72,26 @@ describe('Serve Greenwood With: ', function() { describe('Serve command with SSR route with staticRouter config set', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/artists/`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}/artists/`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('

Analog

'); - expect(response.body).to.contain('Analog'); + expect(body).to.contain('

Analog

'); + expect(body).to.contain('Analog'); done(); }); }); diff --git a/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js b/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js index 039d37c33..4e9ce751b 100644 --- a/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js +++ b/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js @@ -26,7 +26,6 @@ import chai from 'chai'; import path from 'path'; import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -67,40 +66,31 @@ describe('Serve Greenwood With: ', function() { describe('Serve command with API specific behaviors for a JSON API', function() { const name = 'Greenwood'; let response = {}; + let data; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/api/greeting?name=${name}`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = JSON.parse(body); - - resolve(); - }); - }); + response = await fetch(`${hostname}/api/greeting?name=${name}`); + data = await response.clone().json(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return a default status message', function(done) { // OK appears to be a Koa default when statusText is an empty string - expect(response.statusMessage).to.equal('OK'); + expect(response.statusText).to.equal('OK'); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('application/json'); + expect(response.headers.get('content-type')).to.equal('application/json'); done(); }); it('should return the correct response body', function(done) { - expect(response.body.message).to.equal(`Hello ${name}!!!`); + expect(data.message).to.equal(`Hello ${name}!!!`); done(); }); }); @@ -108,39 +98,30 @@ describe('Serve Greenwood With: ', function() { describe('Serve command with API specific behaviors for an HTML ("fragment") API', function() { const name = 'Greenwood'; let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/api/fragment?name=${name}`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}/api/fragment?name=${name}`); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return a custom status message', function(done) { - expect(response.statusMessage).to.equal('SUCCESS!!!'); + expect(response.statusText).to.equal('SUCCESS!!!'); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return the correct response body', function(done) { - expect(response.body).to.contain(`

Hello ${name}!!!

`); + expect(body).to.contain(`

Hello ${name}!!!

`); done(); }); }); @@ -149,20 +130,11 @@ describe('Serve Greenwood With: ', function() { let response = {}; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/api/nothing`, (err, res) => { - if (err) { - reject(); - } - - response = res; - resolve(); - }); - }); + response = await fetch(`${hostname}/api/nothing`); }); it('should return a custom status code', function(done) { - expect(response.statusCode).to.equal(204); + expect(response.status).to.equal(204); done(); }); }); @@ -171,51 +143,31 @@ describe('Serve Greenwood With: ', function() { let response = {}; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/api/foo`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}/api/foo`); }); it('should return a 404 status', function(done) { - expect(response.statusCode).to.equal(404); + expect(response.status).to.equal(404); done(); }); }); describe('Serve command with API specific behaviors with a custom response', function() { let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/api/missing`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(); - }); - }); + response = await fetch(`${hostname}/api/missing`); + body = await response.clone().text(); }); it('should return a 404 status', function(done) { - expect(response.statusCode).to.equal(404); + expect(response.status).to.equal(404); done(); }); it('should return a body of not found', function(done) { - expect(response.body).to.equal('Not Found'); + expect(body).to.equal('Not Found'); done(); }); }); @@ -223,47 +175,35 @@ describe('Serve Greenwood With: ', function() { describe('Serve command with POST API specific behaviors for JSON', function() { const param = 'Greenwood'; let response = {}; + let data; before(async function() { - return new Promise((resolve, reject) => { - request.post({ - url: `${hostname}/api/submit-json`, - body: { - name: param - }, - json: true - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(response); - }); + response = await fetch(`${hostname}/api/submit-json`, { + method: 'POST', + body: JSON.stringify({ name: param }) }); + data = await response.clone().json(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); - it('should return the expected response message', function(done) { - const { message } = response.body; - - expect(message).to.equal(`Thank you ${param} for your submission!`); + it('should return the expected content type header', function(done) { + expect(response.headers.get('content-type')).to.equal('application/json'); done(); }); - it('should return the expected content type header', function(done) { - expect(response.headers['content-type']).to.equal('application/json'); + it('should return the secret header in the response', function(done) { + expect(response.headers.get('x-secret')).to.equal('1234'); done(); }); - it('should return the secret header in the response', function(done) { - expect(response.headers['x-secret']).to.equal('1234'); + it('should return the expected response message', function(done) { + const { message } = data; + + expect(message).to.equal(`Thank you ${param} for your submission!`); done(); }); }); @@ -271,39 +211,31 @@ describe('Serve Greenwood With: ', function() { describe('Serve command with POST API specific behaviors for FormData', function() { const param = 'Greenwood'; let response = {}; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.post({ - url: `${hostname}/api/submit-form-data`, - form: { - name: param - } - }, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - resolve(response); - }); + response = await fetch(`${hostname}/api/submit-form-data`, { + method: 'POST', + body: new URLSearchParams({ name: param }).toString(), + headers: new Headers({ + 'content-type': 'application/x-www-form-urlencoded' + }) }); + body = await response.clone().text(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); - it('should return the expected response message', function(done) { - expect(response.body).to.equal(`Thank you ${param} for your submission!`); + it('should return the expected content type header', function(done) { + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); - it('should return the expected content type header', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + it('should return the expected response message', function(done) { + expect(body).to.equal(`Thank you ${param} for your submission!`); done(); }); }); diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js index 87c535b13..ecdd62f82 100644 --- a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js +++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js @@ -29,7 +29,6 @@ import glob from 'glob-promise'; import { JSDOM } from 'jsdom'; import path from 'path'; import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -72,36 +71,27 @@ describe('Serve Greenwood With: ', function() { describe('Serve command that prerenders SSR pages', function() { let dom; let response; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - dom = new JSDOM(body); - - resolve(); - }); - }); + response = await fetch(`${hostname}/`); + body = await response.clone().text(); + dom = new JSDOM(body); }); describe('Serve command with HTML response for the home page', function() { it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a response body', function(done) { - expect(response.body).to.not.be.undefined; + expect(body).to.not.be.undefined; done(); }); @@ -140,34 +130,25 @@ describe('Serve Greenwood With: ', function() { describe('Serve command with API specific behaviors for a JSON API', function() { const name = 'Greenwood'; let response = {}; + let data; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/api/greeting?name=${name}`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = JSON.parse(body); - - resolve(); - }); - }); + response = await fetch(`${hostname}/api/greeting?name=${name}`); + data = await response.clone().json(); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('application/json'); + expect(response.headers.get('content-type')).to.equal('application/json'); done(); }); it('should return the correct response body', function(done) { - expect(response.body.message).to.equal(`Hello ${name}!!!`); + expect(data.message).to.equal(`Hello ${name}!!!`); done(); }); }); diff --git a/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js b/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js index 063f98db3..e3d382c5d 100644 --- a/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js +++ b/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js @@ -27,7 +27,6 @@ import glob from 'glob-promise'; import { JSDOM } from 'jsdom'; import path from 'path'; import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -69,36 +68,27 @@ describe('Serve Greenwood With: ', function() { describe('Serve command that prerenders SSR pages', function() { let dom; let response; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - dom = new JSDOM(body); - - resolve(); - }); - }); + response = await fetch(`${hostname}/`); + body = await response.clone().text(); + dom = new JSDOM(body); }); describe('Serve command with HTML response for the home page', function() { it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a response body', function(done) { - expect(response.body).to.not.be.undefined; + expect(body).to.not.be.undefined; done(); }); diff --git a/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js b/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js index 7db6a586f..977b201a6 100644 --- a/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js +++ b/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js @@ -26,7 +26,6 @@ import glob from 'glob-promise'; import { JSDOM } from 'jsdom'; import path from 'path'; import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -147,35 +146,26 @@ describe('Serve Greenwood With: ', function() { describe('Serve command that tests for static HTML export from SSR route', function() { let dom; let response; + let body; before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/artists/`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - dom = new JSDOM(body); - - resolve(); - }); - }); + response = await fetch(`${hostname}/artists/`); + body = await response.clone().text(); + dom = new JSDOM(body); }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a response body', function(done) { - expect(response.body).to.not.be.undefined; + expect(body).to.not.be.undefined; done(); }); diff --git a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js index 9999fb0bb..4490f5e17 100644 --- a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js +++ b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js @@ -35,7 +35,6 @@ import fs from 'fs'; import { JSDOM } from 'jsdom'; import path from 'path'; import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; -import request from 'request'; import { runSmokeTest } from '../../../../../test/smoke-test.js'; import { Runner } from 'gallinago'; import { fileURLToPath } from 'url'; @@ -74,63 +73,18 @@ describe('Serve Greenwood With: ', function() { runSmokeTest(['serve'], LABEL); - let response = {}; - let artistsPageDom; - let homePageDom; - let usersPageDom; - let artistsPageGraphData; - - before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/`, (err, res, body) => { - if (err) { - reject(); - } - - homePageDom = new JSDOM(body); - - resolve(); - }); - }); - }); - - before(async function() { - const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, 'public/graph.json'), 'utf-8')); - - artistsPageGraphData = graph.find(page => page.route === '/artists/'); - - return new Promise((resolve, reject) => { - request.get(`${hostname}/artists/`, (err, res, body) => { - if (err) { - reject(); - } - - response = res; - response.body = body; - - artistsPageDom = new JSDOM(body); - - resolve(); - }); - }); - }); - - before(async function() { - return new Promise((resolve, reject) => { - request.get(`${hostname}/users/`, (err, res, body) => { - if (err) { - reject(); - } - usersPageDom = new JSDOM(body); + describe('Serve command with HTML route response for the home page using "get" functions', function() { + let response; + let dom; - resolve(); - }); + before(async function() { + response = await fetch(`${hostname}/`); + const body = await response.clone().text(); + dom = new JSDOM(body); }); - }); - describe('Serve command with HTML route response for the home page using "get" functions', function() { it('should have the expected output for the page', function() { - const headings = homePageDom.window.document.querySelectorAll('body > h1'); + const headings = dom.window.document.querySelectorAll('body > h1'); expect(headings.length).to.equal(1); expect(headings[0].textContent).to.equal('Hello from the server rendered home page!'); @@ -145,19 +99,32 @@ describe('Serve Greenwood With: ', function() { }); describe('Serve command with HTML route response for artists page using "get" functions', function() { + let response; + let dom; + let artistsPageGraphData; + let body; + + before(async function() { + response = await fetch(`${hostname}/artists/`); + body = await response.clone().text(); + const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, 'public/graph.json'), 'utf-8')); + + artistsPageGraphData = graph.find(page => page.route === '/artists/'); + dom = new JSDOM(body); + }); it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + expect(response.status).to.equal(200); done(); }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers.get('content-type')).to.equal('text/html'); done(); }); it('should return a response body', function(done) { - expect(response.body).to.not.be.undefined; + expect(body).to.not.be.undefined; done(); }); @@ -170,31 +137,31 @@ describe('Serve Greenwood With: ', function() { }); it('the response body should be valid HTML from JSDOM', function(done) { - expect(artistsPageDom).to.not.be.undefined; + expect(dom).to.not.be.undefined; done(); }); it('should have one style tags', function() { - const styles = artistsPageDom.window.document.querySelectorAll('head > style'); + const styles = dom.window.document.querySelectorAll('head > style'); expect(styles.length).to.equal(1); }); it('should have the expected number of