From c7c92a4a077bb06d14a84b9c1c3badfc1bafcc5f Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 27 Apr 2024 21:05:52 -0400 Subject: [PATCH 1/4] map output bundle names and paths to rollup bundle output name --- packages/cli/src/config/rollup.config.js | 25 +++-- packages/cli/src/lifecycles/bundle.js | 9 +- packages/cli/src/lifecycles/graph.js | 2 + packages/cli/src/lifecycles/serve.js | 2 +- .../plugins/resource/plugin-standard-html.js | 2 +- .../cases/develop.ssr/develop.ssr.spec.js | 91 +++++++++++++++++++ .../develop.ssr/src/pages/blog/first-post.js | 7 ++ .../cases/develop.ssr/src/pages/blog/index.js | 7 ++ .../serve.default.ssr.spec.js | 91 +++++++++++++++++++ .../src/pages/blog/first-post.js | 7 ++ .../serve.default.ssr/src/pages/blog/index.js | 7 ++ 11 files changed, 236 insertions(+), 14 deletions(-) create mode 100644 packages/cli/test/cases/develop.ssr/src/pages/blog/first-post.js create mode 100644 packages/cli/test/cases/develop.ssr/src/pages/blog/index.js create mode 100644 packages/cli/test/cases/serve.default.ssr/src/pages/blog/first-post.js create mode 100644 packages/cli/test/cases/serve.default.ssr/src/pages/blog/index.js diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index 8aa6f56c2..0382f0444 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -274,20 +274,29 @@ function greenwoodImportMetaUrl(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() { +function greenwoodPatchSsrPagesEntryPointRuntimeImport(compilation) { return { name: 'greenwood-patch-ssr-pages-entry-point-runtime-import', generateBundle(options, bundle) { Object.keys(bundle).forEach((key) => { - if (key.startsWith('__')) { - // ___GWD_ENTRY_FILE_URL=${filename}___ + // map rollup bundle names back to original SSR pages for output bundles and paths + if (key.startsWith('_')) { const needle = bundle[key].code.match(/___GWD_ENTRY_FILE_URL=(.*.)___/); - if (needle) { + + if (bundle[key].facadeModuleId.startsWith(compilation.context.scratchDir.pathname) && 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 => ${key}`); + Object.keys(bundle).forEach((_) => { + if (bundle[_].facadeModuleId === `${compilation.context.pagesDir.pathname}${entryPathMatch}`) { + bundle[key].code = bundle[key].code.replace(/'___GWD_ENTRY_FILE_URL=(.*.)___'/, `new URL('./${bundle[_].fileName}', import.meta.url)`); + + compilation.graph.forEach((page, idx) => { + if (page.relativeWorkspacePagePath === `/${entryPathMatch}`) { + compilation.graph[idx].outputPath = key; + } + }); + } + }); } } }); @@ -405,7 +414,7 @@ const getRollupConfigForSsr = async (compilation, input) => { }), commonjs(), greenwoodImportMetaUrl(compilation), - greenwoodPatchSsrPagesEntryPointRuntimeImport() // TODO a little hacky but works for now + greenwoodPatchSsrPagesEntryPointRuntimeImport(compilation) // TODO a little hacky but works for now ], onwarn: (errorObj) => { const { code, message } = errorObj; diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index e5a28fdd5..71063e748 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -190,9 +190,9 @@ async function bundleSsrPages(compilation) { for (const page of compilation.graph) { 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 { filename, imports, route, template, title, relativeWorkspacePagePath } = page; + const entryFileUrl = new URL(`./_${relativeWorkspacePagePath.replace('/', '')}`, scratchDir); + const moduleUrl = new URL(`./${relativeWorkspacePagePath.replace('/', '')}`, 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 @@ -206,13 +206,14 @@ async function bundleSsrPages(compilation) { staticHtml = staticHtml.replace(/[`\\$]/g, '\\$&'); // https://stackoverflow.com/a/75688937/417806 // better way to write out this inline code? + await fs.mkdir(entryFileUrl.pathname.replace(filename, ''), { recursive: true }); 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 moduleUrl = '___GWD_ENTRY_FILE_URL=${relativeWorkspacePagePath.replace('/', '')}___'; const data = await executeRouteModule({ moduleUrl, compilation, page, request }); let staticHtml = \`${staticHtml}\`; diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js index ae82285e6..40b95f7a9 100644 --- a/packages/cli/src/lifecycles/graph.js +++ b/packages/cli/src/lifecycles/graph.js @@ -191,6 +191,7 @@ const generateGraph = async (compilation) => { * data: custom page frontmatter * filename: base filename of the page * id: filename without the extension + * relativeWorkspacePagePath: the file path relative to the user's workspace directory * label: "pretty" text representation of the filename * imports: per page JS or CSS file imports to be included in HTML output from frontmatter * resources: sum of all resources for the entire page @@ -206,6 +207,7 @@ const generateGraph = async (compilation) => { data: customData || {}, filename, id, + relativeWorkspacePagePath: relativePagePath, label: id.split('-') .map((idPart) => { return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 678750374..7c94c29a8 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -294,7 +294,7 @@ async function getHybridServer(compilation) { const request = transformKoaRequestIntoStandardRequest(url, ctx.request); if (!config.prerender && matchingRoute.isSSR && !matchingRoute.prerender) { - const { handler } = await import(new URL(`./__${matchingRoute.filename}`, outputDir)); + const { handler } = await import(new URL(`./${matchingRoute.outputPath}`, outputDir)); const response = await handler(request, compilation); ctx.body = Readable.from(response.body); diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 8f0622545..b64fcee64 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -105,7 +105,7 @@ class StandardHtmlResource extends ResourceInterface { } if (matchingRoute.isSSR) { - const routeModuleLocationUrl = new URL(`./${matchingRoute.filename}`, pagesDir); + const routeModuleLocationUrl = new URL(`.${matchingRoute.relativeWorkspacePagePath}`, pagesDir); const routeWorkerUrl = this.compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().executeModuleUrl; await new Promise(async (resolve, reject) => { 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 4f7919f66..18ecbccbf 100644 --- a/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js +++ b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js @@ -16,6 +16,9 @@ * components/ * footer.js * pages/ + * blog + * first-post.js + * index.js * artists.js * post.js * templates/ @@ -284,6 +287,94 @@ describe('Develop Greenwood With: ', function() { expect(paragraph[0].textContent).to.not.be.undefined; }); }); + + describe('Develop command with HTML route response using default export and nested SSR Blog Index page', function() { + let response = {}; + let dom = {}; + let body; + + before(async function() { + response = await fetch(`${hostname}/blog/`); + body = await response.clone().text(); + dom = new JSDOM(body); + }); + + 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.get('content-type')).to.equal('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(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('Nested SSR page should work!'); + }); + }); + + describe('Develop command with HTML route response using default export and nested SSR Blog First Post page', function() { + let response = {}; + let dom = {}; + let body; + + before(async function() { + response = await fetch(`${hostname}/blog/first-post/`); + body = await response.clone().text(); + dom = new JSDOM(body); + }); + + 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.get('content-type')).to.equal('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(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('Nested SSR First Post page should work!'); + }); + }); }); after(function() { diff --git a/packages/cli/test/cases/develop.ssr/src/pages/blog/first-post.js b/packages/cli/test/cases/develop.ssr/src/pages/blog/first-post.js new file mode 100644 index 000000000..d65087023 --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/src/pages/blog/first-post.js @@ -0,0 +1,7 @@ +export default class BlogFirstPostPage extends HTMLElement { + connectedCallback() { + this.innerHTML = ` +

Nested SSR First Post page should work!

+ `; + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/develop.ssr/src/pages/blog/index.js b/packages/cli/test/cases/develop.ssr/src/pages/blog/index.js new file mode 100644 index 000000000..331617673 --- /dev/null +++ b/packages/cli/test/cases/develop.ssr/src/pages/blog/index.js @@ -0,0 +1,7 @@ +export default class BlogPage extends HTMLElement { + connectedCallback() { + this.innerHTML = ` +

Nested SSR page should work!

+ `; + } +} \ No newline at end of file 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 7a4b01b35..393ca909a 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 @@ -23,6 +23,9 @@ * pages/ * about.md * artists.js + * blog/ + * first-post.js + * index.js * index.js * post.js * users.js @@ -272,6 +275,94 @@ describe('Serve Greenwood With: ', function() { }); }); + describe('Serve command with HTML route response using default export and nested SSR Blog Index page', function() { + let response = {}; + let dom = {}; + let body; + + before(async function() { + response = await fetch(`${hostname}/blog/`); + body = await response.clone().text(); + dom = new JSDOM(body); + }); + + 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.get('content-type')).to.equal('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(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('Nested SSR page should work!'); + }); + }); + + describe('Develop command with HTML route response using default export and nested SSR Blog First Post page', function() { + let response = {}; + let dom = {}; + let body; + + before(async function() { + response = await fetch(`${hostname}/blog/first-post/`); + body = await response.clone().text(); + dom = new JSDOM(body); + }); + + 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.get('content-type')).to.equal('text/html'); + done(); + }); + + it('should return a response body', function(done) { + expect(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('Nested SSR First Post page should work!'); + }); + }); + describe('Bundled image using new URL and import.meta.url', function() { const bundledName = 'assets/logo-abb2e884.svg'; let response = {}; diff --git a/packages/cli/test/cases/serve.default.ssr/src/pages/blog/first-post.js b/packages/cli/test/cases/serve.default.ssr/src/pages/blog/first-post.js new file mode 100644 index 000000000..d65087023 --- /dev/null +++ b/packages/cli/test/cases/serve.default.ssr/src/pages/blog/first-post.js @@ -0,0 +1,7 @@ +export default class BlogFirstPostPage extends HTMLElement { + connectedCallback() { + this.innerHTML = ` +

Nested SSR First Post page should work!

+ `; + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default.ssr/src/pages/blog/index.js b/packages/cli/test/cases/serve.default.ssr/src/pages/blog/index.js new file mode 100644 index 000000000..331617673 --- /dev/null +++ b/packages/cli/test/cases/serve.default.ssr/src/pages/blog/index.js @@ -0,0 +1,7 @@ +export default class BlogPage extends HTMLElement { + connectedCallback() { + this.innerHTML = ` +

Nested SSR page should work!

+ `; + } +} \ No newline at end of file From 6779ca92d32a6c028ed59f1fa87b46f75ede45ed Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 27 Apr 2024 21:13:44 -0400 Subject: [PATCH 2/4] document relativeWorkspacePagePath --- www/pages/docs/data.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/pages/docs/data.md b/www/pages/docs/data.md index 530afc393..c7e60cfe4 100644 --- a/www/pages/docs/data.md +++ b/www/pages/docs/data.md @@ -62,6 +62,8 @@ graph { outputPath, // (string) the relative path to write to when generating static HTML + relativeWorkspacePagePath, // the file path relative to the user's workspace directory + path, // (string) path to the file route, // (string) A URL, typically derived from the filesystem path, e.g. /blog/2019/first-post/ From 61c4c3ec6aeacaa05d020799cf98ba2064ac9cef Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 3 May 2024 18:25:14 -0400 Subject: [PATCH 3/4] window support refactorings --- packages/cli/src/config/rollup.config.js | 8 ++++++-- packages/cli/src/lifecycles/bundle.js | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index 0382f0444..dc0d582d6 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -278,16 +278,20 @@ function greenwoodPatchSsrPagesEntryPointRuntimeImport(compilation) { return { name: 'greenwood-patch-ssr-pages-entry-point-runtime-import', generateBundle(options, bundle) { + const { pagesDir, scratchDir } = compilation.context; + Object.keys(bundle).forEach((key) => { // map rollup bundle names back to original SSR pages for output bundles and paths if (key.startsWith('_')) { const needle = bundle[key].code.match(/___GWD_ENTRY_FILE_URL=(.*.)___/); - if (bundle[key].facadeModuleId.startsWith(compilation.context.scratchDir.pathname) && needle) { + // handle windows shenanigans for facadeModuleId and path separators + if (new URL(`file://${bundle[key].facadeModuleId}`).pathname.startsWith(scratchDir.pathname) && needle) { const entryPathMatch = needle[1]; Object.keys(bundle).forEach((_) => { - if (bundle[_].facadeModuleId === `${compilation.context.pagesDir.pathname}${entryPathMatch}`) { + // handle windows shenanigans for facadeModuleId and path separators + if (new URL(`file://${bundle[_].facadeModuleId}`).pathname === `${pagesDir.pathname}${entryPathMatch}`) { bundle[key].code = bundle[key].code.replace(/'___GWD_ENTRY_FILE_URL=(.*.)___'/, `new URL('./${bundle[_].fileName}', import.meta.url)`); compilation.graph.forEach((page, idx) => { diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index 71063e748..4aad70159 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -190,9 +190,10 @@ async function bundleSsrPages(compilation) { for (const page of compilation.graph) { if (page.isSSR && !page.prerender) { - const { filename, imports, route, template, title, relativeWorkspacePagePath } = page; + const { imports, route, template, title, relativeWorkspacePagePath } = page; const entryFileUrl = new URL(`./_${relativeWorkspacePagePath.replace('/', '')}`, scratchDir); const moduleUrl = new URL(`./${relativeWorkspacePagePath.replace('/', '')}`, pagesDir); + const outputPathRootUrl = new URL(`file://${path.dirname(entryFileUrl.pathname)}`); 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 @@ -205,8 +206,13 @@ async function bundleSsrPages(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 + if (!await checkResourceExists(outputPathRootUrl)) { + await fs.mkdir(outputPathRootUrl, { + recursive: true + }); + } + // better way to write out this inline code? - await fs.mkdir(entryFileUrl.pathname.replace(filename, ''), { recursive: true }); await fs.writeFile(entryFileUrl, ` import { executeRouteModule } from '${normalizePathnameForWindows(executeModuleUrl)}'; From ac2d3f2e65b781194792819e3321337b3a6f5670 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 3 May 2024 18:55:20 -0400 Subject: [PATCH 4/4] refactor theme pack specs to support latest Node 18.20.x --- .../theme-pack-context-plugin.js | 13 ++++--------- .../develop.plugins.context/greenwood.config.js | 4 +++- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js b/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js index bff573f54..2227fbad2 100644 --- a/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js +++ b/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js @@ -1,20 +1,15 @@ import fs from 'fs/promises'; -import os from 'os'; -import { spawnSync } from 'child_process'; const packageJson = JSON.parse(await fs.readFile(new URL('./package.json', import.meta.url), 'utf-8')); -const myThemePackPlugin = () => [{ +const myThemePackPlugin = (options = {}) => [{ type: 'context', name: 'my-theme-pack:context', provider: () => { const { name } = packageJson; - const command = os.platform() === 'win32' ? 'npm.cmd' : 'npm'; - const ls = spawnSync(command, ['ls', name]); - const isInstalled = ls.stdout.toString().indexOf('(empty)') < 0; - const templateLocation = isInstalled - ? new URL(`./node_modules/${name}/dist/layouts/`, import.meta.url) - : new URL('./fixtures/layouts/', import.meta.url); + const templateLocation = options.__isDevelopment // eslint-disable-line no-underscore-dangle + ? new URL('./fixtures/layouts/', import.meta.url) + : new URL(`./node_modules/${name}/dist/layouts/`, import.meta.url); return { templates: [ diff --git a/packages/cli/test/cases/develop.plugins.context/greenwood.config.js b/packages/cli/test/cases/develop.plugins.context/greenwood.config.js index 74c311257..60eaf3c94 100644 --- a/packages/cli/test/cases/develop.plugins.context/greenwood.config.js +++ b/packages/cli/test/cases/develop.plugins.context/greenwood.config.js @@ -24,7 +24,9 @@ class MyThemePackDevelopmentResource extends ResourceInterface { export default { plugins: [ - ...myThemePackPlugin(), + ...myThemePackPlugin({ + __isDevelopment: true + }), { type: 'resource', name: 'my-theme-pack:resource',