From 8bd46fd9b3a91bfa73e9ebd6ca24de25d4f821f8 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 22 Jun 2024 12:49:40 -0400 Subject: [PATCH] Rfcs/issue 955 layouts and pages (#1212) * feature/discussion 1117 Isolation Mode (v1) (#1206) * isolation mode for SSR pages and API routes for greenwood serve * documentation for isolation mode option and global config test case * misc refactoring * set isolation mode to true for Lit renderer plugin * set isolation mode to true for Lit renderer plugin * enhancement/Issue-1118: Single File Bundles for SSR and API routes (#1186) * Issue-1118: Refactor rollup config generation for APIs * Issue-1118: Refactor rollup config generation for SSR * Issue-1118: Refactor forEach to use for-in for the ssr config generation * Issue-1118: Convert forEach to for..in * Issue-1118: Remove unused code * refactor away bundling work arounds and add comments * refactor SSR page bundling to avoid hacky entry point placeholder hack * patch custom element registry check from wcc * refactor SSR page output name from .entry to .route * document breaking changes for adapter plugins * refactor import meta relative asset path escaping * refactor API routes and adapters for mapped API bundles * misc refactoring and docs update * latest WCC patches * windows compatibility * update adapter docs example * remove patches --------- Co-authored-by: Owen Buckley * v0.30.0-alpha.1 * feature/issue 923 native import attributes for CSS and JSON (#1215) * intial draft of import attributes support for CSS and JSON * all test cases passing * need patch package * wcc patches for import attributes and CSSStylesheet shim * bump min NodeJS version for exp specs * temp disable ESLint * develop based import assertion specs * serve based import attributes specs * add preIntercept resource plugin lifecycle and refactor PostCSS to use it * all test cases passing for import attributes support * refactor built in CSS and JSON intercepting * demo code * raw plugin docs and package.json updates * update latest documentation for custom loaders support in NodeJS * update custom import docs * upgrade wcc v0.13.0 * only need Node 18 for github actions * css imports and raw plugin interop with test cases * lit renderer import attribute test cases and documentation * refactor matchers support for raw plugin instead of patching and add test cases * disable describe.only * update usage for custom resource plugins to showcase usage of import attributes * document preIntercept lifecycle and convert Babel to use it * restore ESLint * enable debug logging for failing specs * refactor theme pack specs * fix linting * remove CSS and JSON packages from being publishable * clean up console logs and comments * rename exp test cases to loadersnaming prefix * fix command in github actions * remove plugin-import-css callout from plugin-postcss README * remove demo code from website * refine PostCSS plugin intercepting * first pass on resource tracking and bundling refactor with lit polyfills removal from CLI * interim work around to solve double rendering and undefined WCC bugs * refactor frontmatter for graph and standard html plugin for SSR * rename templates directory to layouts * refactor over bundling of static script assets in bundleSsrPages * handle bundling styles within bundleSsrPages * post rebase tweaks * custom elements as layouts * post rebase tweaks * WCC patched support for TS pages * support and tests for API routes as a custom dynamic format * restore TS tests and make servePage default * document custom page format support * fix lint * patch latest WCC TypeScript changes * cleanup default app layout content * handle rollup circular dependency warnings for API routes * rename test cases from templates to layout * collapse API routes directory into pages directory * bump to wc-compiler 0.14.0 --------- Co-authored-by: Paul Barry --- .github/CONTRIBUTING.md | 2 +- packages/cli/package.json | 2 +- packages/cli/src/commands/build.js | 64 +-- packages/cli/src/config/rollup.config.js | 31 +- .../cli/src/{templates => layouts}/404.html | 0 .../cli/src/{templates => layouts}/app.html | 0 .../cli/src/{templates => layouts}/page.html | 0 packages/cli/src/lib/execute-route-module.js | 8 +- packages/cli/src/lib/layout-utils.js | 281 +++++++++++ packages/cli/src/lib/resource-utils.js | 7 +- packages/cli/src/lib/router.js | 6 +- packages/cli/src/lib/ssr-route-worker.js | 2 +- packages/cli/src/lib/templating-utils.js | 195 ------- packages/cli/src/lifecycles/bundle.js | 144 +++--- packages/cli/src/lifecycles/compile.js | 2 +- packages/cli/src/lifecycles/config.js | 12 +- packages/cli/src/lifecycles/context.js | 12 +- packages/cli/src/lifecycles/graph.js | 476 +++++++++--------- packages/cli/src/lifecycles/prerender.js | 30 +- packages/cli/src/lifecycles/serve.js | 2 +- .../src/plugins/resource/plugin-api-routes.js | 2 +- .../plugins/resource/plugin-standard-html.js | 62 +-- .../plugins/resource/plugin-static-router.js | 12 +- ...ld.config.error-layouts-directory.spec.js} | 10 +- .../greenwood.config.js | 3 + .../greenwood.config.js | 3 - ...ild.config.interpolate-frontmatter.spec.js | 2 +- .../src/{templates => layouts}/blog.html | 0 .../src/pages/blog/first-post.md | 2 +- .../build.config.layouts-directory.spec.js} | 8 +- .../greenwood.config.js | 3 + .../src/my-layouts}/page.html | 2 +- .../src/pages/index.md | 3 + .../build.config-optimization-inline.spec.js | 2 + .../src/{templates => layouts}/app.html | 0 .../build.config.prerender.spec.js | 6 - .../build.config.static-router.spec.js | 6 +- .../src/pages/about.md | 2 +- .../src/pages/regex-test.html | 2 +- .../greenwood.config.js | 3 - .../src/pages/index.md | 3 - .../build.default.meta.spec.js | 4 +- .../src/{templates => layouts}/page.html | 0 .../build.default.ssr-prerender.spec.js | 34 +- .../src/components/footer.js | 15 +- .../src/components/header.js | 20 + .../src/components/social-links.js | 13 + .../src/layouts}/app.html | 6 +- .../src/pages/index.js | 3 + .../build.default.ssr-static-export.spec.js | 2 +- .../src/{templates => layouts}/app.html | 0 .../src/pages/artists.js | 4 +- .../build.default.title.spec.js | 4 +- .../src/{templates => layouts}/page.html | 0 ...ild.default.workspace-404-markdown.spec.js | 22 +- .../src/{templates => layouts}/app.html | 0 .../build.default.workspace-404.spec.js | 22 +- .../src/{templates => layouts}/app.html | 0 ....default.workspace-getting-started.spec.js | 2 +- .../src/{templates => layouts}/blog.html | 0 .../src/{templates => layouts}/page.html | 0 .../src/pages/blog/first-post.md | 2 +- .../src/pages/blog/second-post.md | 2 +- ...ild.default.workspace-layouts-app.spec.js} | 16 +- .../src/layouts}/app.html | 2 +- ...d.default.workspace-layouts-empty.spec.js} | 8 +- .../src/pages/index.html | 0 .../src/pages/no-body.html | 0 .../src/pages/no-head.html | 0 .../src/pages/shell.html | 0 .../src/scripts/main.js | 1 + .../src/styles/main.css | 0 ...space-layouts-page-and-app-dynamic.spec.js | 105 ++++ .../src/layouts/app.js | 20 + .../src/layouts/page.js | 10 + .../src/pages/index.md | 3 + ...lt.workspace-layouts-page-and-app.spec.js} | 52 +- .../src/layouts}/app.html | 12 +- .../src/layouts/page.html | 37 ++ .../src/scripts/app-layout-one.js | 1 + .../src/scripts/app-layout-two.js | 1 + .../src/scripts/page-layout-one.js | 1 + .../src/scripts/page-layout-two.js | 1 + .../src/styles/app-layout-one.css} | 0 .../src/styles/app-layout-two.css} | 0 .../src/styles/page-layout-one.css} | 0 .../src/styles/page-layout-two.css} | 0 ...rkspace-layouts-page-bare-merging.spec.js} | 6 +- .../src/layouts}/page.html | 2 +- .../src/pages/index.md | 0 ...ld.default.workspace-layouts-page.spec.js} | 24 +- .../greenwood.config.js | 0 .../src/layouts}/page.html | 0 .../src/scripts/main.js | 0 .../src/styles/theme.css | 0 ....workspace-layouts-relative-paths.spec.js} | 8 +- .../source-sans-pro-v13-latin-regular.ttf | Bin .../source-sans-pro-v13-latin-regular.woff | Bin .../source-sans-pro-v13-latin-regular.woff2 | Bin .../src/assets/fonts/source-sans-pro.css | 0 .../src/components/footer.js | 0 .../src/components/greeting.js | 0 .../src/components/header.js | 0 .../src/layouts}/app.html | 0 .../src/layouts}/page.html | 0 .../src/pages/index.html | 0 .../src/pages/one/two/three/index.md | 0 .../src/styles/home.css | 0 .../src/styles/page.css | 0 .../src/styles/theme.css | 0 .../src/scripts/app-template-one.js | 1 - .../src/scripts/app-template-two.js | 1 - .../src/scripts/page-template-one.js | 1 - .../src/scripts/page-template-two.js | 1 - .../src/templates/page.html | 37 -- .../src/scripts/main.js | 1 - ...t.workspace-user-directory-mapping.spec.js | 2 +- .../src/{templates => layouts}/page.html | 0 .../cases/build.default/build.default.spec.js | 8 - .../build.config.plugins-adapter.spec.js | 8 +- .../src/{ => pages}/api/greeting.js | 0 .../build.plugins.context.spec.js | 36 +- .../fixtures/{layouts => my-layouts}/app.html | 2 +- .../{layouts => my-layouts}/page.html | 2 +- .../fixtures/my-layouts}/title.html | 2 +- .../src/pages/slides/index.md | 2 +- .../theme-pack-context-plugin.js | 10 +- .../build.plugins-source.spec.js | 8 +- .../build.plugins.source/greenwood.config.js | 2 +- .../src/{templates => layouts}/artist.html | 0 .../develop.config.base-path.spec.js | 4 +- .../src/{ => pages}/api/greeting.js | 0 .../develop.default/develop.default.spec.js | 16 +- .../src/{ => pages}/api/fragment.js | 2 +- .../src/{ => pages}/api/greeting.js | 0 .../src/{ => pages}/api/missing.js | 0 .../src/{ => pages}/api/nothing.js | 0 .../src/{ => pages}/api/submit-form-data.js | 0 .../src/{ => pages}/api/submit-json.js | 0 .../develop.plugins.context.spec.js | 12 +- .../fixtures/{layouts => my-layouts}/app.html | 2 +- .../{layouts => my-layouts}/page.html | 2 +- .../fixtures/my-layouts}/title.html | 2 +- .../src/pages/slides/index.md | 2 +- .../cases/develop.ssr/develop.ssr.spec.js | 4 +- .../src/{templates => layouts}/app.html | 0 .../cases/develop.ssr/src/pages/artists.js | 4 +- .../contact.bar | 7 + .../greenwood.config.js | 44 ++ ...oaders-build.plugins.resource-page.spec.js | 114 +++++ .../src/pages/about.foo | 5 + .../src/pages/index.html | 8 + ...ders-develop.ssr-import-attributes.spec.js | 4 +- .../src/{ => pages}/api/fragment.js | 2 +- ...erve.default.ssr-import-attributes.spec.js | 4 +- .../src/{ => pages}/api/fragment.js | 2 +- .../serve.config.base-path.spec.js | 10 +- .../serve.config.base-path/src/pages/about.md | 2 +- .../src/{ => pages}/api/greeting.js | 0 .../src/pages/about.md | 2 +- .../serve.default.api.spec.js | 18 +- .../src/{ => pages}/api/fragment.js | 2 +- .../src/{ => pages}/api/greeting.js | 0 .../src/{ => pages}/api/missing.js | 0 .../src/{ => pages}/api/nothing.js | 0 .../src/{ => pages}/api/submit-form-data.js | 0 .../src/{ => pages}/api/submit-json.js | 0 ...e.default.ssr-prerender-api-hybrid.spec.js | 6 +- .../src/layouts}/app.html | 0 .../src/{ => pages}/api/greeting.js | 0 .../serve.default.ssr-prerender.spec.js | 8 +- .../src/layouts}/app.html | 0 .../serve.default.ssr-static-export.spec.js | 4 +- .../src/{templates => layouts}/app.html | 0 .../src/pages/artists.js | 4 +- .../serve.default.ssr.spec.js | 6 +- .../src/{templates => layouts}/app.html | 0 .../serve.default.ssr/src/pages/artists.js | 4 +- .../serve.default/src/assets/router.js.map | 2 +- .../test/cases/theme-pack/my-theme-pack.js | 10 +- .../{layouts => my-layouts}/blog-post.html | 2 +- .../test/cases/theme-pack/src/pages/index.md | 2 +- .../cases/theme-pack/theme-pack.build.spec.js | 18 +- .../theme-pack/theme-pack.develop.spec.js | 6 +- .../build.config.base-path.spec.js | 4 +- .../src/{ => pages}/api/greeting.js | 0 .../cases/build.default/build.default.spec.js | 12 +- .../src/{ => pages}/api/fragment.js | 4 +- .../build.default/src/pages}/api/greeting.js | 2 +- .../src/{ => pages}/api/search.js | 4 +- .../src/{ => pages}/api/submit-form-data.js | 0 .../src/{ => pages}/api/submit-json.js | 0 .../build.config.base-path.spec.js | 4 +- .../src/{ => pages}/api/greeting.js | 0 .../cases/build.default/build.default.spec.js | 12 +- .../src/{ => pages}/api/fragment.js | 4 +- .../build.default/src/pages}/api/greeting.js | 2 +- .../src/{ => pages}/api/search.js | 4 +- .../src/{ => pages}/api/submit-form-data.js | 0 .../src/{ => pages}/api/submit-json.js | 0 packages/plugin-graphql/README.md | 2 +- .../plugin-graphql/src/queries/children.gql | 2 +- packages/plugin-graphql/src/queries/graph.gql | 2 +- packages/plugin-graphql/src/schema/graph.js | 2 +- .../query-graph/src/pages/blog/first-post.md | 2 +- .../query-graph/src/pages/blog/second-post.md | 2 +- .../test/cases/query-menu/query-menu.spec.js | 2 +- .../src/{templates => layouts}/page.html | 0 .../plugin-graphql/test/unit/common.spec.js | 8 +- .../plugin-graphql/test/unit/mocks/graph.js | 90 ++-- .../test/unit/schema/graph.menu.spec.js | 12 +- .../test/unit/schema/graph.spec.js | 6 +- .../exp-build.prerender.spec.js | 2 +- .../src/{templates => layouts}/app.html | 0 .../exp-build.prerender.spec.js | 2 +- .../src/{templates => layouts}/app.html | 0 packages/plugin-import-jsx/package.json | 2 +- .../cases/default/default.prerender.spec.js | 2 +- .../src/{templates => layouts}/app.html | 0 .../loaders-build.prerender.spec.js | 2 +- .../src/{templates => layouts}/app.html | 0 .../loaders-build.prerender.spec.js | 2 +- .../src/{templates => layouts}/app.html | 0 .../loaders-serve.ssr.spec.js | 4 +- .../src/{ => pages}/api/fragment.js | 4 +- packages/plugin-renderer-lit/README.md | 2 +- .../src/execute-route-module.js | 10 +- .../build.prerender.getting-started.spec.js | 4 +- .../src/{templates => layouts}/blog.html | 0 .../src/{templates => layouts}/page.html | 0 .../src/pages/blog/first-post.md | 2 +- .../src/pages/blog/second-post.md | 2 +- .../cases/serve.default/serve.default.spec.js | 9 +- .../src/{templates => layouts}/app.html | 0 .../src/{ => pages}/api/search.js | 4 +- .../cases/serve.default/src/pages/artists.js | 4 +- .../cases/build.default/build.default.spec.js | 6 - packages/plugin-typescript/README.md | 32 +- packages/plugin-typescript/src/index.js | 3 +- .../greenwood.config.js | 8 + .../loaders-build.resource-page.spec.js | 100 ++++ .../src/components/greeting.ts | 17 + .../src/pages/about.ts | 10 + .../src/pages/index.html | 8 + .../loaders-develop.ssr/greenwood.config.js | 9 + .../loaders-develop.ssr.spec.js | 140 ++++++ .../cases/loaders-develop.ssr/package.json | 4 + .../src/components/greeting.ts | 17 + .../src/pages/api/greeting.ts | 17 + .../loaders-develop.ssr/src/pages/index.ts | 10 + .../loaders-serve.prerender-ssr.spec.js | 19 +- .../loaders-serve.ssr/greenwood.config.js | 2 +- .../loaders-serve.ssr.spec.js | 48 +- .../src/components/greeting.ts | 17 + .../src/{ => pages}/api/fragment.js | 2 +- .../src/pages/api/greeting.ts | 17 + www/{templates => layouts}/app.html | 0 www/{templates => layouts}/blog.html | 0 www/{templates => layouts}/page.html | 0 www/pages/about/features.md | 2 +- www/pages/blog/index.md | 2 +- www/pages/blog/release/v0-15-0.md | 2 +- www/pages/blog/release/v0-18-0.md | 2 +- www/pages/blog/release/v0-19-0.md | 2 +- www/pages/blog/release/v0-20-0.md | 2 +- www/pages/blog/release/v0-21-0.md | 2 +- www/pages/blog/release/v0-23-0.md | 2 +- www/pages/blog/release/v0-24-0.md | 2 +- www/pages/blog/release/v0-26-0.md | 2 +- www/pages/blog/release/v0-27-0.md | 2 +- www/pages/blog/release/v0-28-0.md | 2 +- www/pages/blog/release/v0-29-0.md | 2 +- www/pages/blog/state-of-greenwood-2022.md | 2 +- www/pages/blog/state-of-greenwood-2023.md | 2 +- www/pages/docs/api-routes.md | 11 +- www/pages/docs/component-model.md | 4 +- www/pages/docs/configuration.md | 18 +- www/pages/docs/css-and-images.md | 2 +- www/pages/docs/data.md | 18 +- www/pages/docs/front-matter.md | 12 +- www/pages/docs/index.md | 2 +- www/pages/docs/layouts.md | 56 +-- www/pages/docs/markdown.md | 2 +- www/pages/docs/scripts.md | 2 +- www/pages/docs/server-rendering.md | 24 +- www/pages/getting-started/branding.md | 4 +- www/pages/getting-started/creating-content.md | 30 +- www/pages/getting-started/index.md | 2 +- www/pages/getting-started/key-concepts.md | 18 +- www/pages/getting-started/quick-start.md | 2 +- www/pages/guides/netlify-cms.md | 6 +- www/pages/guides/theme-packs.md | 28 +- www/pages/plugins/context.md | 16 +- www/pages/plugins/index.md | 4 +- www/pages/plugins/renderer.md | 2 +- www/pages/plugins/resource.md | 3 + yarn.lock | 134 ++++- 297 files changed, 2229 insertions(+), 1273 deletions(-) rename packages/cli/src/{templates => layouts}/404.html (100%) rename packages/cli/src/{templates => layouts}/app.html (100%) rename packages/cli/src/{templates => layouts}/page.html (100%) create mode 100644 packages/cli/src/lib/layout-utils.js delete mode 100644 packages/cli/src/lib/templating-utils.js rename packages/cli/test/cases/{build.config.error-templates-directory/build.config.error-templates-directory.spec.js => build.config.error-layouts-directory/build.config.error-layouts-directory.spec.js} (63%) create mode 100644 packages/cli/test/cases/build.config.error-layouts-directory/greenwood.config.js delete mode 100644 packages/cli/test/cases/build.config.error-templates-directory/greenwood.config.js rename packages/cli/test/cases/build.config.interpolate-frontmatter/src/{templates => layouts}/blog.html (100%) rename packages/cli/test/cases/{build.config.templates-directory/build.config.templates-directory.spec.js => build.config.layouts-directory/build.config.layouts-directory.spec.js} (89%) create mode 100644 packages/cli/test/cases/build.config.layouts-directory/greenwood.config.js rename packages/cli/test/cases/{build.config.templates-directory/src/layouts => build.config.layouts-directory/src/my-layouts}/page.html (66%) create mode 100644 packages/cli/test/cases/build.config.layouts-directory/src/pages/index.md rename packages/cli/test/cases/build.config.optimization-inline/src/{templates => layouts}/app.html (100%) delete mode 100644 packages/cli/test/cases/build.config.templates-directory/greenwood.config.js delete mode 100644 packages/cli/test/cases/build.config.templates-directory/src/pages/index.md rename packages/cli/test/cases/build.default.meta/src/{templates => layouts}/page.html (100%) create mode 100644 packages/cli/test/cases/build.default.ssr-prerender/src/components/header.js create mode 100644 packages/cli/test/cases/build.default.ssr-prerender/src/components/social-links.js rename packages/cli/test/cases/{serve.default.ssr-prerender/src/templates => build.default.ssr-prerender/src/layouts}/app.html (64%) rename packages/cli/test/cases/build.default.ssr-static-export/src/{templates => layouts}/app.html (100%) rename packages/cli/test/cases/build.default.title/src/{templates => layouts}/page.html (100%) rename packages/cli/test/cases/build.default.workspace-404-markdown/src/{templates => layouts}/app.html (100%) rename packages/cli/test/cases/build.default.workspace-404/src/{templates => layouts}/app.html (100%) rename packages/cli/test/cases/build.default.workspace-getting-started/src/{templates => layouts}/blog.html (100%) rename packages/cli/test/cases/build.default.workspace-getting-started/src/{templates => layouts}/page.html (100%) rename packages/cli/test/cases/{build.default.workspace-template-app/build.default.workspace-template-app.spec.js => build.default.workspace-layouts-app/build.default.workspace-layouts-app.spec.js} (85%) rename packages/cli/test/cases/{build.default.workspace-template-app/src/templates => build.default.workspace-layouts-app/src/layouts}/app.html (88%) rename packages/cli/test/cases/{build.default.workspace-templates-empty/build.default.workspace-templates-empty.spec.js => build.default.workspace-layouts-empty/build.default.workspace-layouts-empty.spec.js} (95%) rename packages/cli/test/cases/{build.default.workspace-templates-empty => build.default.workspace-layouts-empty}/src/pages/index.html (100%) rename packages/cli/test/cases/{build.default.workspace-templates-empty => build.default.workspace-layouts-empty}/src/pages/no-body.html (100%) rename packages/cli/test/cases/{build.default.workspace-templates-empty => build.default.workspace-layouts-empty}/src/pages/no-head.html (100%) rename packages/cli/test/cases/{build.default.workspace-templates-empty => build.default.workspace-layouts-empty}/src/pages/shell.html (100%) create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-empty/src/scripts/main.js rename packages/cli/test/cases/{build.default.workspace-templates-empty => build.default.workspace-layouts-empty}/src/styles/main.css (100%) create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app-dynamic/build.default.workspace-layouts-page-and-app-dynamic.spec.js create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app-dynamic/src/layouts/app.js create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app-dynamic/src/layouts/page.js create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app-dynamic/src/pages/index.md rename packages/cli/test/cases/{build.default.workspace-template-page-and-app/build.default.workspace-template-page-and-app.spec.js => build.default.workspace-layouts-page-and-app/build.default.workspace-layouts-page-and-app.spec.js} (75%) rename packages/cli/test/cases/{build.default.workspace-template-page-and-app/src/templates => build.default.workspace-layouts-page-and-app/src/layouts}/app.html (61%) create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app/src/layouts/page.html create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app/src/scripts/app-layout-one.js create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app/src/scripts/app-layout-two.js create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app/src/scripts/page-layout-one.js create mode 100644 packages/cli/test/cases/build.default.workspace-layouts-page-and-app/src/scripts/page-layout-two.js rename packages/cli/test/cases/{build.default.workspace-template-page-and-app/src/styles/app-template-one.css => build.default.workspace-layouts-page-and-app/src/styles/app-layout-one.css} (100%) rename packages/cli/test/cases/{build.default.workspace-template-page-and-app/src/styles/app-template-two.css => build.default.workspace-layouts-page-and-app/src/styles/app-layout-two.css} (100%) rename packages/cli/test/cases/{build.default.workspace-template-page-and-app/src/styles/page-template-one.css => build.default.workspace-layouts-page-and-app/src/styles/page-layout-one.css} (100%) rename packages/cli/test/cases/{build.default.workspace-template-page-and-app/src/styles/page-template-two.css => build.default.workspace-layouts-page-and-app/src/styles/page-layout-two.css} (100%) rename packages/cli/test/cases/{build.default.workspace-template-page-bare-merging/build.default.workspace-template-page-bare-merging.spec.js => build.default.workspace-layouts-page-bare-merging/build.default.workspace-layouts-page-bare-merging.spec.js} (95%) rename packages/cli/test/cases/{build.default.workspace-template-page-bare-merging/src/templates => build.default.workspace-layouts-page-bare-merging/src/layouts}/page.html (80%) rename packages/cli/test/cases/{build.default.workspace-template-page-bare-merging => build.default.workspace-layouts-page-bare-merging}/src/pages/index.md (100%) rename packages/cli/test/cases/{build.default.workspace-template-page/build.default.workspace-template-page.spec.js => build.default.workspace-layouts-page/build.default.workspace-layouts-page.spec.js} (88%) rename packages/cli/test/cases/{build.default.workspace-template-page => build.default.workspace-layouts-page}/greenwood.config.js (100%) rename packages/cli/test/cases/{build.default.workspace-template-page/src/templates => build.default.workspace-layouts-page/src/layouts}/page.html (100%) rename packages/cli/test/cases/{build.default.workspace-template-page => build.default.workspace-layouts-page}/src/scripts/main.js (100%) rename packages/cli/test/cases/{build.default.workspace-template-page => build.default.workspace-layouts-page}/src/styles/theme.css (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths/build.default.workspace-templates-relative-paths.spec.js => build.default.workspace-layouts-relative-paths/build.default.workspace-layouts-relative-paths.spec.js} (97%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/assets/fonts/source-sans-pro-v13-latin-regular.ttf (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/assets/fonts/source-sans-pro-v13-latin-regular.woff (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/assets/fonts/source-sans-pro-v13-latin-regular.woff2 (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/assets/fonts/source-sans-pro.css (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/components/footer.js (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/components/greeting.js (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/components/header.js (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths/src/templates => build.default.workspace-layouts-relative-paths/src/layouts}/app.html (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths/src/templates => build.default.workspace-layouts-relative-paths/src/layouts}/page.html (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/pages/index.html (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/pages/one/two/three/index.md (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/styles/home.css (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/styles/page.css (100%) rename packages/cli/test/cases/{build.default.workspace-templates-relative-paths => build.default.workspace-layouts-relative-paths}/src/styles/theme.css (100%) delete mode 100644 packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/app-template-one.js delete mode 100644 packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/app-template-two.js delete mode 100644 packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/page-template-one.js delete mode 100644 packages/cli/test/cases/build.default.workspace-template-page-and-app/src/scripts/page-template-two.js delete mode 100644 packages/cli/test/cases/build.default.workspace-template-page-and-app/src/templates/page.html delete mode 100644 packages/cli/test/cases/build.default.workspace-templates-empty/src/scripts/main.js rename packages/cli/test/cases/build.default.workspace-user-directory-mapping/src/{templates => layouts}/page.html (100%) rename packages/cli/test/cases/build.plugins.adapter/src/{ => pages}/api/greeting.js (100%) rename packages/cli/test/cases/build.plugins.context/fixtures/{layouts => my-layouts}/app.html (82%) rename packages/cli/test/cases/build.plugins.context/fixtures/{layouts => my-layouts}/page.html (55%) rename packages/cli/test/cases/{develop.plugins.context/fixtures/layouts => build.plugins.context/fixtures/my-layouts}/title.html (72%) rename packages/cli/test/cases/build.plugins.source/src/{templates => layouts}/artist.html (100%) rename packages/cli/test/cases/develop.config.base-path/src/{ => pages}/api/greeting.js (100%) rename packages/cli/test/cases/develop.default/src/{ => pages}/api/fragment.js (89%) rename packages/cli/test/cases/develop.default/src/{ => pages}/api/greeting.js (100%) rename packages/cli/test/cases/develop.default/src/{ => pages}/api/missing.js (100%) rename packages/cli/test/cases/develop.default/src/{ => pages}/api/nothing.js (100%) rename packages/cli/test/cases/develop.default/src/{ => pages}/api/submit-form-data.js (100%) rename packages/cli/test/cases/develop.default/src/{ => pages}/api/submit-json.js (100%) rename packages/cli/test/cases/develop.plugins.context/fixtures/{layouts => my-layouts}/app.html (82%) rename packages/cli/test/cases/develop.plugins.context/fixtures/{layouts => my-layouts}/page.html (55%) rename packages/cli/test/cases/{build.plugins.context/fixtures/layouts => develop.plugins.context/fixtures/my-layouts}/title.html (72%) rename packages/cli/test/cases/develop.ssr/src/{templates => layouts}/app.html (100%) create mode 100644 packages/cli/test/cases/loaders-build.plugins.resource-page/contact.bar create mode 100644 packages/cli/test/cases/loaders-build.plugins.resource-page/greenwood.config.js create mode 100644 packages/cli/test/cases/loaders-build.plugins.resource-page/loaders-build.plugins.resource-page.spec.js create mode 100644 packages/cli/test/cases/loaders-build.plugins.resource-page/src/pages/about.foo create mode 100644 packages/cli/test/cases/loaders-build.plugins.resource-page/src/pages/index.html rename packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/{ => pages}/api/fragment.js (87%) rename packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/{ => pages}/api/fragment.js (87%) rename packages/cli/test/cases/serve.config.base-path/src/{ => pages}/api/greeting.js (100%) rename packages/cli/test/cases/serve.default.api/src/{ => pages}/api/fragment.js (89%) rename packages/cli/test/cases/serve.default.api/src/{ => pages}/api/greeting.js (100%) rename packages/cli/test/cases/serve.default.api/src/{ => pages}/api/missing.js (100%) rename packages/cli/test/cases/serve.default.api/src/{ => pages}/api/nothing.js (100%) rename packages/cli/test/cases/serve.default.api/src/{ => pages}/api/submit-form-data.js (100%) rename packages/cli/test/cases/serve.default.api/src/{ => pages}/api/submit-json.js (100%) rename packages/cli/test/cases/{build.default.ssr-prerender/src/templates => serve.default.ssr-prerender-api-hybrid/src/layouts}/app.html (100%) rename packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/{ => pages}/api/greeting.js (100%) rename packages/cli/test/cases/{serve.default.ssr-prerender-api-hybrid/src/templates => serve.default.ssr-prerender/src/layouts}/app.html (100%) rename packages/cli/test/cases/serve.default.ssr-static-export/src/{templates => layouts}/app.html (100%) rename packages/cli/test/cases/serve.default.ssr/src/{templates => layouts}/app.html (100%) rename packages/cli/test/cases/theme-pack/src/{layouts => my-layouts}/blog-post.html (79%) rename packages/plugin-adapter-netlify/test/cases/build.config.base-path/src/{ => pages}/api/greeting.js (100%) rename packages/plugin-adapter-netlify/test/cases/build.default/src/{ => pages}/api/fragment.js (86%) rename packages/{plugin-adapter-vercel/test/cases/build.default/src => plugin-adapter-netlify/test/cases/build.default/src/pages}/api/greeting.js (87%) rename packages/plugin-adapter-netlify/test/cases/build.default/src/{ => pages}/api/search.js (88%) rename packages/plugin-adapter-netlify/test/cases/build.default/src/{ => pages}/api/submit-form-data.js (100%) rename packages/plugin-adapter-netlify/test/cases/build.default/src/{ => pages}/api/submit-json.js (100%) rename packages/plugin-adapter-vercel/test/cases/build.config.base-path/src/{ => pages}/api/greeting.js (100%) rename packages/plugin-adapter-vercel/test/cases/build.default/src/{ => pages}/api/fragment.js (86%) rename packages/{plugin-adapter-netlify/test/cases/build.default/src => plugin-adapter-vercel/test/cases/build.default/src/pages}/api/greeting.js (87%) rename packages/plugin-adapter-vercel/test/cases/build.default/src/{ => pages}/api/search.js (88%) rename packages/plugin-adapter-vercel/test/cases/build.default/src/{ => pages}/api/submit-form-data.js (100%) rename packages/plugin-adapter-vercel/test/cases/build.default/src/{ => pages}/api/submit-json.js (100%) rename packages/plugin-graphql/test/cases/query-menu/src/{templates => layouts}/page.html (100%) rename packages/plugin-import-css/test/cases/exp-build.prerender/src/{templates => layouts}/app.html (100%) rename packages/plugin-import-json/test/cases/exp-build.prerender/src/{templates => layouts}/app.html (100%) rename packages/plugin-import-jsx/test/cases/default/src/{templates => layouts}/app.html (100%) rename packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/{templates => layouts}/app.html (100%) rename packages/plugin-import-raw/test/cases/loaders-build.prerender/src/{templates => layouts}/app.html (100%) rename packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/{ => pages}/api/fragment.js (82%) rename packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/{templates => layouts}/blog.html (100%) rename packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/src/{templates => layouts}/page.html (100%) rename packages/plugin-renderer-lit/test/cases/serve.default/src/{templates => layouts}/app.html (100%) rename packages/plugin-renderer-lit/test/cases/serve.default/src/{ => pages}/api/search.js (88%) create mode 100644 packages/plugin-typescript/test/cases/loaders-build.resource-page/greenwood.config.js create mode 100644 packages/plugin-typescript/test/cases/loaders-build.resource-page/loaders-build.resource-page.spec.js create mode 100644 packages/plugin-typescript/test/cases/loaders-build.resource-page/src/components/greeting.ts create mode 100644 packages/plugin-typescript/test/cases/loaders-build.resource-page/src/pages/about.ts create mode 100644 packages/plugin-typescript/test/cases/loaders-build.resource-page/src/pages/index.html create mode 100644 packages/plugin-typescript/test/cases/loaders-develop.ssr/greenwood.config.js create mode 100644 packages/plugin-typescript/test/cases/loaders-develop.ssr/loaders-develop.ssr.spec.js create mode 100644 packages/plugin-typescript/test/cases/loaders-develop.ssr/package.json create mode 100644 packages/plugin-typescript/test/cases/loaders-develop.ssr/src/components/greeting.ts create mode 100644 packages/plugin-typescript/test/cases/loaders-develop.ssr/src/pages/api/greeting.ts create mode 100644 packages/plugin-typescript/test/cases/loaders-develop.ssr/src/pages/index.ts create mode 100644 packages/plugin-typescript/test/cases/loaders-serve.ssr/src/components/greeting.ts rename packages/plugin-typescript/test/cases/loaders-serve.ssr/src/{ => pages}/api/fragment.js (91%) create mode 100644 packages/plugin-typescript/test/cases/loaders-serve.ssr/src/pages/api/greeting.ts rename www/{templates => layouts}/app.html (100%) rename www/{templates => layouts}/blog.html (100%) rename www/{templates => layouts}/page.html (100%) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d3e92b598..90ed23df3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -57,7 +57,7 @@ The [layout](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/ - _lib/_ - Custom utility and client facing files - _lifecycles/_ - Tasks that can be composed by commands to support the full needs of that command - _plugins/_ - Custom default plugins maintained by the CLI project -- _templates/_ - Default templates and / or pages provided by Greenwood. +- _layouts/_ - Default layouts and / or pages provided by Greenwood. #### Lifecycles diff --git a/packages/cli/package.json b/packages/cli/package.json index 2b70e5367..66b577095 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,7 +52,7 @@ "remark-rehype": "^7.0.0", "rollup": "^3.29.4", "unified": "^9.2.0", - "wc-compiler": "~0.13.0" + "wc-compiler": "~0.14.0" }, "devDependencies": { "@babel/runtime": "^7.10.4", diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js index e2cc575fe..82937397c 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.js @@ -1,70 +1,10 @@ import { bundleCompilation } from '../lifecycles/bundle.js'; -import { checkResourceExists, trackResourcesForRoute } from '../lib/resource-utils.js'; +import { checkResourceExists } from '../lib/resource-utils.js'; import { copyAssets } from '../lifecycles/copy.js'; import fs from 'fs/promises'; import { preRenderCompilationWorker, preRenderCompilationCustom, staticRenderCompilation } from '../lifecycles/prerender.js'; import { ServerInterface } from '../lib/server-interface.js'; -// TODO a lot of these are duplicated in the prerender lifecycle too -// would be good to refactor -async function servePage(url, request, plugins) { - let response = new Response(''); - - for (const plugin of plugins) { - if (plugin.shouldServe && await plugin.shouldServe(url, request)) { - response = await plugin.serve(url, request); - break; - } - } - - return response; -} - -async function interceptPage(url, request, plugins, body) { - let response = new Response(body, { - headers: new Headers({ 'Content-Type': 'text/html' }) - }); - - for (const plugin of plugins) { - if (plugin.shouldPreIntercept && await plugin.shouldPreIntercept(url, request, response)) { - response = await plugin.preIntercept(url, request, response); - } - - if (plugin.shouldIntercept && await plugin.shouldIntercept(url, request, response)) { - response = await plugin.intercept(url, request, response); - } - } - - return response; -} - -function getPluginInstances (compilation) { - return [...compilation.config.plugins] - .filter(plugin => plugin.type === 'resource' && plugin.name !== 'plugin-node-modules:resource') - .map((plugin) => { - return plugin.provider(compilation); - }); -} - -// TODO does this make more sense in bundle lifecycle? -// https://github.com/ProjectEvergreen/greenwood/issues/970 -// or could this be done sooner (like in appTemplate building in html resource plugin)? -// Or do we need to ensure userland code / plugins have gone first -async function trackResourcesForRoutes(compilation) { - const plugins = getPluginInstances(compilation); - - for (const page of compilation.graph) { - const { route } = page; - 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 trackResourcesForRoute(body, compilation, route); - } -} - const runProductionBuild = async (compilation) => { return new Promise(async (resolve, reject) => { @@ -106,13 +46,11 @@ const runProductionBuild = async (compilation) => { })); if (prerenderPlugin.executeModuleUrl) { - await trackResourcesForRoutes(compilation); await preRenderCompilationWorker(compilation, prerenderPlugin); } else { await preRenderCompilationCustom(compilation, prerenderPlugin); } } else { - await trackResourcesForRoutes(compilation); await staticRenderCompilation(compilation); } diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index 87a8fddcd..3bb9b8ce3 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -423,10 +423,10 @@ const getRollupConfigForScriptResources = async (compilation) => { }; const getRollupConfigForApis = async (compilation) => { - const { outputDir, userWorkspace } = compilation.context; + const { outputDir, pagesDir } = compilation.context; return [...compilation.manifest.apis.values()] - .map(api => normalizePathnameForWindows(new URL(`.${api.path}`, userWorkspace))) + .map(api => normalizePathnameForWindows(new URL(`.${api.path}`, pagesDir))) .map(filepath => ({ input: filepath, output: { @@ -445,7 +445,27 @@ const getRollupConfigForApis = async (compilation) => { }), commonjs(), greenwoodImportMetaUrl(compilation) - ] + ], + onwarn: (errorObj) => { + const { code, message } = errorObj; + + switch (code) { + + case 'CIRCULAR_DEPENDENCY': + // let this through for WCC + sucrase + // Circular dependency: ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js -> + // ../../../../../node_modules/sucrase/dist/esm/parser/traverser/util.js -> ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js + // Circular dependency: ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js -> + // ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/readWord.js -> ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js + // https://github.com/ProjectEvergreen/greenwood/pull/1212 + // https://github.com/lit/lit/issues/449#issuecomment-416688319 + break; + default: + // otherwise, log all warnings from rollup + console.debug(message); + + } + } })); }; @@ -483,11 +503,12 @@ const getRollupConfigForSsr = async (compilation, input) => { switch (code) { case 'CIRCULAR_DEPENDENCY': - // TODO let this through for lit by suppressing it + // let this through for lit // 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 + // https://github.com/lit/lit/issues/449#issuecomment-416688319 + // https://github.com/rollup/rollup/issues/1089#issuecomment-402109607 break; default: // otherwise, log all warnings from rollup diff --git a/packages/cli/src/templates/404.html b/packages/cli/src/layouts/404.html similarity index 100% rename from packages/cli/src/templates/404.html rename to packages/cli/src/layouts/404.html diff --git a/packages/cli/src/templates/app.html b/packages/cli/src/layouts/app.html similarity index 100% rename from packages/cli/src/templates/app.html rename to packages/cli/src/layouts/app.html diff --git a/packages/cli/src/templates/page.html b/packages/cli/src/layouts/page.html similarity index 100% rename from packages/cli/src/templates/page.html rename to packages/cli/src/layouts/page.html diff --git a/packages/cli/src/lib/execute-route-module.js b/packages/cli/src/lib/execute-route-module.js index 1d3746ace..dec53e5e2 100644 --- a/packages/cli/src/lib/execute-route-module.js +++ b/packages/cli/src/lib/execute-route-module.js @@ -2,7 +2,7 @@ import { renderToString, renderFromHTML } from 'wc-compiler'; async function executeRouteModule({ moduleUrl, compilation, page = {}, prerender = false, htmlContents = null, scripts = [], request }) { const data = { - template: null, + layout: null, body: null, frontmatter: null, html: null @@ -15,7 +15,7 @@ async function executeRouteModule({ moduleUrl, compilation, page = {}, prerender data.html = html; } else { const module = await import(moduleUrl).then(module => module); - const { prerender = false, getTemplate = null, getBody = null, getFrontmatter = null, isolation } = module; + const { prerender = false, getLayout = null, getBody = null, getFrontmatter = null, isolation } = module; if (module.default) { const { html } = await renderToString(new URL(moduleUrl), false, request); @@ -27,8 +27,8 @@ async function executeRouteModule({ moduleUrl, compilation, page = {}, prerender } } - if (getTemplate) { - data.template = await getTemplate(compilation, page); + if (getLayout) { + data.layout = await getLayout(compilation, page); } if (getFrontmatter) { diff --git a/packages/cli/src/lib/layout-utils.js b/packages/cli/src/lib/layout-utils.js new file mode 100644 index 000000000..e39739676 --- /dev/null +++ b/packages/cli/src/lib/layout-utils.js @@ -0,0 +1,281 @@ +import fs from 'fs/promises'; +import htmlparser from 'node-html-parser'; +import { checkResourceExists } from './resource-utils.js'; +import { Worker } from 'worker_threads'; + +async function getCustomPageLayoutsFromPlugins(compilation, layoutName) { + // TODO confirm context plugins work for SSR + // TODO support context plugins for more than just HTML files + const contextPlugins = compilation.config.plugins.filter((plugin) => { + return plugin.type === 'context'; + }).map((plugin) => { + return plugin.provider(compilation); + }); + + const customLayoutLocations = []; + const layoutDir = contextPlugins + .map(plugin => plugin.layouts) + .flat(); + + for (const layoutDirUrl of layoutDir) { + if (layoutName) { + const layoutUrl = new URL(`./${layoutName}.html`, layoutDirUrl); + + if (await checkResourceExists(layoutUrl)) { + customLayoutLocations.push(layoutUrl); + } + } + } + + return customLayoutLocations; +} + +async function getPageLayout(filePath, compilation, layout) { + const { config, context } = compilation; + const { layoutsDir, userLayoutsDir, pagesDir, projectDirectory } = context; + const filePathUrl = new URL(`${filePath}`, projectDirectory); + const customPageFormatPlugins = config.plugins + .filter(plugin => plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin) + .map(plugin => plugin.provider(compilation)); + const isCustomStaticPage = customPageFormatPlugins[0] + && customPageFormatPlugins[0].servePage === 'static' + && customPageFormatPlugins[0].shouldServe + && await customPageFormatPlugins[0].shouldServe(filePathUrl); + const customPluginDefaultPageLayouts = await getCustomPageLayoutsFromPlugins(compilation, 'page'); + const customPluginPageLayouts = await getCustomPageLayoutsFromPlugins(compilation, layout); + const extension = filePath.split('.').pop(); + const is404Page = filePath.startsWith('404') && extension === 'html'; + const hasCustomStaticLayout = await checkResourceExists(new URL(`./${layout}.html`, userLayoutsDir)); + const hasCustomDynamicLayout = await checkResourceExists(new URL(`./${layout}.js`, userLayoutsDir)); + const hasPageLayout = await checkResourceExists(new URL('./page.html', userLayoutsDir)); + const hasCustom404Page = await checkResourceExists(new URL('./404.html', pagesDir)); + const isHtmlPage = extension === 'html' && await checkResourceExists(new URL(`./${filePath}`, projectDirectory)); + let contents; + + if (layout && (customPluginPageLayouts.length > 0 || hasCustomStaticLayout)) { + // use a custom layout, usually from markdown frontmatter + contents = customPluginPageLayouts.length > 0 + ? await fs.readFile(new URL(`./${layout}.html`, customPluginPageLayouts[0]), 'utf-8') + : await fs.readFile(new URL(`./${layout}.html`, userLayoutsDir), 'utf-8'); + } else if (isHtmlPage) { + // if the page is already HTML, use that as the layout, NOT accounting for 404 pages + contents = await fs.readFile(filePathUrl, 'utf-8'); + } else if (isCustomStaticPage) { + // transform, then use that as the layout, NOT accounting for 404 pages + const transformed = await customPageFormatPlugins[0].serve(filePathUrl); + contents = await transformed.text(); + } else if (customPluginDefaultPageLayouts.length > 0 || (!is404Page && hasPageLayout)) { + // else look for default page layout from the user + // and 404 pages should be their own "top level" layout + contents = customPluginDefaultPageLayouts.length > 0 + ? await fs.readFile(new URL('./page.html', customPluginDefaultPageLayouts[0]), 'utf-8') + : await fs.readFile(new URL('./page.html', userLayoutsDir), 'utf-8'); + } else if (hasCustomDynamicLayout && !is404Page) { + const routeModuleLocationUrl = new URL(`./${layout}.js`, userLayoutsDir); + const routeWorkerUrl = compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().executeModuleUrl; + + await new Promise(async (resolve, reject) => { + const worker = new Worker(new URL('./ssr-route-worker.js', import.meta.url)); + + worker.on('message', (result) => { + + if (result.body) { + contents = result.body; + } + resolve(); + }); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Worker stopped with exit code ${code}`)); + } + }); + + worker.postMessage({ + executeModuleUrl: routeWorkerUrl.href, + moduleUrl: routeModuleLocationUrl.href, + compilation: JSON.stringify(compilation) + }); + }); + } else if (is404Page && !hasCustom404Page) { + contents = await fs.readFile(new URL('./404.html', layoutsDir), 'utf-8'); + } else { + // fallback to using Greenwood's stock page layout + contents = await fs.readFile(new URL('./page.html', layoutsDir), 'utf-8'); + } + + return contents; +} + +/* eslint-disable-next-line complexity */ +async function getAppLayout(pageLayoutContents, compilation, customImports = [], frontmatterTitle) { + const enableHud = compilation.config.devServer.hud; + const { layoutsDir, userLayoutsDir } = compilation.context; + const userStaticAppLayoutUrl = new URL('./app.html', userLayoutsDir); + // TODO support more than just .js files + const userDynamicAppLayoutUrl = new URL('./app.js', userLayoutsDir); + const userHasStaticAppLayout = await checkResourceExists(userStaticAppLayoutUrl); + const userHasDynamicAppLayout = await checkResourceExists(userDynamicAppLayoutUrl); + const customAppLayoutsFromPlugins = await getCustomPageLayoutsFromPlugins(compilation, 'app'); + let dynamicAppLayoutContents; + + if (userHasDynamicAppLayout) { + const routeWorkerUrl = compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().executeModuleUrl; + + await new Promise(async (resolve, reject) => { + const worker = new Worker(new URL('./ssr-route-worker.js', import.meta.url)); + + worker.on('message', (result) => { + + if (result.body) { + dynamicAppLayoutContents = result.body; + } + resolve(); + }); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Worker stopped with exit code ${code}`)); + } + }); + + worker.postMessage({ + executeModuleUrl: routeWorkerUrl.href, + moduleUrl: userDynamicAppLayoutUrl.href, + compilation: JSON.stringify(compilation) + }); + }); + } + + let appLayoutContents = customAppLayoutsFromPlugins.length > 0 + ? await fs.readFile(new URL('./app.html', customAppLayoutsFromPlugins[0])) + : userHasStaticAppLayout + ? await fs.readFile(userStaticAppLayoutUrl, 'utf-8') + : userHasDynamicAppLayout + ? dynamicAppLayoutContents + : await fs.readFile(new URL('./app.html', layoutsDir), 'utf-8'); + let mergedLayoutContents = ''; + + const pageRoot = pageLayoutContents && htmlparser.parse(pageLayoutContents, { + script: true, + style: true, + noscript: true, + pre: true + }); + const appRoot = htmlparser.parse(appLayoutContents, { + script: true, + style: true + }); + + if ((pageLayoutContents && !pageRoot.valid) || !appRoot.valid) { + console.debug('ERROR: Invalid HTML detected'); + const invalidContents = !pageRoot.valid + ? pageLayoutContents + : appLayoutContents; + + if (enableHud) { + appLayoutContents = appLayoutContents.replace('', ` + +
+

Malformed HTML detected, please check your closing tags or an HTML formatter.

+
+
+                ${invalidContents.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"')}
+              
+
+
+ `); + } + + mergedLayoutContents = appLayoutContents.replace(/<\/page-outlet>/, ''); + } else { + const appTitle = appRoot ? appRoot.querySelector('head title') : null; + const appBody = appRoot.querySelector('body') ? appRoot.querySelector('body').innerHTML : ''; + const pageBody = pageRoot && pageRoot.querySelector('body') ? pageRoot.querySelector('body').innerHTML : ''; + const pageTitle = pageRoot && pageRoot.querySelector('head title'); + const hasInterpolatedFrontmatter = pageTitle && pageTitle.rawText.indexOf('${globalThis.page.title}') >= 0 + || appTitle && appTitle.rawText.indexOf('${globalThis.page.title}') >= 0; + + const title = hasInterpolatedFrontmatter // favor frontmatter interpolation first + ? pageTitle && pageTitle.rawText + ? pageTitle.rawText + : appTitle.rawText + : frontmatterTitle // otherwise, work in order of specificity from page -> page layout -> app layout + ? frontmatterTitle + : pageTitle && pageTitle.rawText + ? pageTitle.rawText + : appTitle && appTitle.rawText + ? appTitle.rawText + : 'My App'; + + const mergedHtml = pageRoot && pageRoot.querySelector('html').rawAttrs !== '' + ? `` + : appRoot.querySelector('html').rawAttrs !== '' + ? `` + : ''; + + const mergedMeta = [ + ...appRoot.querySelectorAll('head meta'), + ...[...(pageRoot && pageRoot.querySelectorAll('head meta')) || []] + ].join('\n'); + + const mergedLinks = [ + ...appRoot.querySelectorAll('head link'), + ...[...(pageRoot && pageRoot.querySelectorAll('head link')) || []] + ].join('\n'); + + const mergedStyles = [ + ...appRoot.querySelectorAll('head style'), + ...[...(pageRoot && pageRoot.querySelectorAll('head style')) || []], + ...customImports.filter(resource => resource.split('.').pop() === 'css') + .map(resource => ``) + ].join('\n'); + + const mergedScripts = [ + ...appRoot.querySelectorAll('head script'), + ...[...(pageRoot && pageRoot.querySelectorAll('head script')) || []], + ...customImports.filter(resource => resource.split('.').pop() === 'js') + .map(resource => ``) + ].join('\n'); + + const finalBody = pageLayoutContents + ? appBody.replace(/<\/page-outlet>/, pageBody) + : appBody; + + mergedLayoutContents = ` + ${mergedHtml} + + ${title} + ${mergedMeta} + ${mergedLinks} + ${mergedStyles} + ${mergedScripts} + + + ${finalBody} + + + `; + } + + return mergedLayoutContents; +} + +async function getUserScripts (contents, compilation) { + const { config } = compilation; + + contents = contents.replace('', ` + + + `); + + return contents; +} + +export { + getAppLayout, + getPageLayout, + getUserScripts +}; \ No newline at end of file diff --git a/packages/cli/src/lib/resource-utils.js b/packages/cli/src/lib/resource-utils.js index a08cd9c57..6a78e4647 100644 --- a/packages/cli/src/lib/resource-utils.js +++ b/packages/cli/src/lib/resource-utils.js @@ -86,7 +86,7 @@ async function checkResourceExists(url) { // turn relative paths into relatively absolute based on a known root directory // * deep link route - /blog/releases/some-post -// * and a nested path in the template - ../../styles/theme.css +// * and a nested path in the layout - ../../styles/theme.css // so will get resolved as `${rootUrl}/styles/theme.css` async function resolveForRelativeUrl(url, rootUrl) { const search = url.search || ''; @@ -111,11 +111,6 @@ async function resolveForRelativeUrl(url, rootUrl) { return reducedUrl; } -// does this make more sense in bundle lifecycle? -// https://github.com/ProjectEvergreen/greenwood/issues/970 -// or could this be done sooner (like in appTemplate building in html resource plugin)? -// Or do we need to ensure userland code / plugins have gone first -// before we can curate the final list of `) - ].join('\n'); - - const finalBody = pageTemplateContents - ? appBody.replace(/<\/page-outlet>/, pageBody) - : appBody; - - mergedTemplateContents = ` - ${mergedHtml} - - ${title} - ${mergedMeta} - ${mergedLinks} - ${mergedStyles} - ${mergedScripts} - - - ${finalBody} - - - `; - } - - return mergedTemplateContents; -} - -async function getUserScripts (contents, compilation) { - const { config } = compilation; - - contents = contents.replace('', ` - - - `); - - return contents; -} - -export { - getAppTemplate, - getPageTemplate, - getUserScripts -}; \ No newline at end of file diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index 352fe485a..33319cfa2 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -1,9 +1,9 @@ /* 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 { getAppLayout, getPageLayout, getUserScripts } from '../lib/layout-utils.js'; import { hashString } from '../lib/hashing-utils.js'; -import { checkResourceExists, mergeResponse, normalizePathnameForWindows } from '../lib/resource-utils.js'; +import { checkResourceExists, mergeResponse, normalizePathnameForWindows, trackResourcesForRoute } from '../lib/resource-utils.js'; import path from 'path'; import { rollup } from 'rollup'; @@ -217,84 +217,96 @@ async function bundleApiRoutes(compilation) { } } -async function bundleSsrPages(compilation) { - // https://rollupjs.org/guide/en/#differences-to-the-javascript-api - // TODO context plugins for SSR ? - // const contextPlugins = compilation.config.plugins.filter((plugin) => { - // return plugin.type === 'context'; - // }).map((plugin) => { - // return plugin.provider(compilation); - // }); - const hasSSRPages = compilation.graph.filter(page => page.isSSR).length > 0; +async function bundleSsrPages(compilation, optimizePlugins) { + const { context, config } = compilation; + const ssrPages = compilation.graph.filter(page => page.isSSR && !page.prerender); + const ssrPrerenderPagesRouteMapper = {}; const input = []; - 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(); + if (!config.prerender && ssrPages.length > 0) { + const { executeModuleUrl } = 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.prerender) { - const { filename, imports, route, template, title, relativeWorkspacePagePath } = page; - const entryFileUrl = new URL(`.${relativeWorkspacePagePath}`, scratchDir); - const moduleUrl = new URL(`.${relativeWorkspacePagePath}`, 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 - const data = await executeRouteModule({ moduleUrl, compilation, page, prerender: false, htmlContents: null, scripts: [], request }); - const pagesPathDiff = compilation.context.pagesDir.pathname.replace(compilation.context.projectDirectory.pathname, ''); - const relativeDepth = relativeWorkspacePagePath.replace(`/${filename}`, '') === '' - ? '../' - : '../'.repeat(relativeWorkspacePagePath.replace(`/${filename}`, '').split('/').length); - 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); - staticHtml = await (await interceptPage(new URL(`http://localhost:8080${route}`), new Request(new URL(`http://localhost:8080${route}`)), getPluginInstances(compilation), staticHtml)).text(); - 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 - }); - } + const { pagesDir, scratchDir } = context; + + // one pass to generate initial static HTML and to track all combined static resources across layouts + // and before we optimize so that all bundled assets can tracked up front + // would be nice to see if this can be done in a single pass though... + for (const page of ssrPages) { + const { imports, route, layout, title, relativeWorkspacePagePath } = page; + const moduleUrl = new URL(`.${relativeWorkspacePagePath}`, pagesDir); + const request = new Request(moduleUrl); + // TODO getLayout 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: [], request }); + let staticHtml = ''; + + staticHtml = data.layout ? data.layout : await getPageLayout(staticHtml, compilation, layout); + staticHtml = await getAppLayout(staticHtml, compilation, imports, title); + staticHtml = await getUserScripts(staticHtml, compilation); + staticHtml = await (await interceptPage(new URL(`http://localhost:8080${route}`), new Request(new URL(`http://localhost:8080${route}`)), getPluginInstances(compilation), staticHtml)).text(); + + await trackResourcesForRoute(staticHtml, compilation, route); + + ssrPrerenderPagesRouteMapper[route] = staticHtml; + } - // better way to write out this inline code? - // using a URL here produces a bundled chunk, but at leasts its bundled - await fs.writeFile(entryFileUrl, ` - import { executeRouteModule } from '${normalizePathnameForWindows(executeModuleUrl)}'; + // technically this happens in the start of bundleCompilation once + // so might be nice to detect those static assets to see if they have be "de-duped" from bundling here + await bundleScriptResources(compilation); + await bundleStyleResources(compilation, optimizePlugins); + + // second pass to link all bundled assets to their resources before optimizing and generating SSR bundles + for (const page of ssrPages) { + const { filename, route, relativeWorkspacePagePath } = page; + const entryFileUrl = new URL(`.${relativeWorkspacePagePath}`, scratchDir); + const outputPathRootUrl = new URL(`file://${path.dirname(entryFileUrl.pathname)}`); + const htmlOptimizer = config.plugins.find(plugin => plugin.name === 'plugin-standard-html').provider(compilation); + const pagesPathDiff = context.pagesDir.pathname.replace(context.projectDirectory.pathname, ''); + const relativeDepth = relativeWorkspacePagePath.replace(`/${filename}`, '') === '' + ? '../' + : '../'.repeat(relativeWorkspacePagePath.replace(`/${filename}`, '').split('/').length); + + let staticHtml = ssrPrerenderPagesRouteMapper[route]; + 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 - const moduleUrl = new URL('${relativeDepth}${pagesPathDiff}${relativeWorkspacePagePath.replace('/', '')}', import.meta.url); + if (!await checkResourceExists(outputPathRootUrl)) { + await fs.mkdir(outputPathRootUrl, { + recursive: true + }); + } - export async function handler(request) { - const compilation = JSON.parse('${JSON.stringify(compilation)}'); - const page = JSON.parse('${JSON.stringify(page)}'); - const data = await executeRouteModule({ moduleUrl, compilation, page, request }); - let staticHtml = \`${staticHtml}\`; + // better way to write out this inline code? + await fs.writeFile(entryFileUrl, ` + import { executeRouteModule } from '${normalizePathnameForWindows(executeModuleUrl)}'; - if (data.body) { - staticHtml = staticHtml.replace(\/\(.*)<\\/content-outlet>\/s, data.body); - } + const moduleUrl = new URL('${relativeDepth}${pagesPathDiff}${relativeWorkspacePagePath.replace('/', '')}', import.meta.url); + + export async function handler(request) { + const compilation = JSON.parse('${JSON.stringify(compilation)}'); + const page = JSON.parse('${JSON.stringify(page)}'); + const data = await executeRouteModule({ moduleUrl, compilation, page, request }); + let staticHtml = \`${staticHtml}\`; - return new Response(staticHtml, { - headers: { - 'Content-Type': 'text/html' - } - }); + if (data.body) { + staticHtml = staticHtml.replace(\/\(.*)<\\/content-outlet>\/s, data.body); } - `); - input.push(normalizePathnameForWindows(entryFileUrl)); - } + return new Response(staticHtml, { + headers: { + 'Content-Type': 'text/html' + } + }); + } + `); + + input.push(normalizePathnameForWindows(entryFileUrl)); } const ssrConfigs = await getRollupConfigForSsr(compilation, input); if (ssrConfigs.length > 0 && ssrConfigs[0].input !== '') { + console.info('bundling dynamic pages...'); for (const configIndex in ssrConfigs) { const rollupConfig = ssrConfigs[configIndex]; const bundle = await rollup(rollupConfig); @@ -337,7 +349,7 @@ const bundleCompilation = async (compilation) => { ]); // bundleSsrPages depends on bundleScriptResources having run first - await bundleSsrPages(compilation); + await bundleSsrPages(compilation, optimizeResourcePlugins); console.info('optimizing static pages....'); await optimizeStaticPages(compilation, optimizeResourcePlugins); diff --git a/packages/cli/src/lifecycles/compile.js b/packages/cli/src/lifecycles/compile.js index 8b09af2a7..fb48336fe 100644 --- a/packages/cli/src/lifecycles/compile.js +++ b/packages/cli/src/lifecycles/compile.js @@ -22,7 +22,7 @@ const generateCompilation = () => { console.info('Initializing project config'); compilation.config = await initConfig(); - // determine whether to use default template or user detected workspace + // determine whether to use default layout or user detected workspace console.info('Initializing project workspace contexts'); compilation.context = await initContext(compilation); diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js index e15fcc998..7ee7fc2f7 100644 --- a/packages/cli/src/lifecycles/config.js +++ b/packages/cli/src/lifecycles/config.js @@ -52,7 +52,7 @@ const defaultConfig = { prerender: false, isolation: false, pagesDirectory: 'pages', - templatesDirectory: 'templates' + layoutsDirectory: 'layouts' }; const readAndMergeConfig = async() => { @@ -77,7 +77,7 @@ const readAndMergeConfig = async() => { if (hasConfigFile) { const userCfgFile = (await import(configUrl)).default; - const { workspace, devServer, markdown, optimization, plugins, port, prerender, basePath, staticRouter, pagesDirectory, templatesDirectory, interpolateFrontmatter, isolation } = userCfgFile; + const { workspace, devServer, markdown, optimization, plugins, port, prerender, basePath, staticRouter, pagesDirectory, layoutsDirectory, interpolateFrontmatter, isolation } = userCfgFile; // workspace validation if (workspace) { @@ -205,10 +205,10 @@ const readAndMergeConfig = async() => { reject(`Error: provided pagesDirectory "${pagesDirectory}" is not supported. Please make sure to pass something like 'docs/'`); } - if (templatesDirectory && typeof templatesDirectory === 'string') { - customConfig.templatesDirectory = templatesDirectory; - } else if (templatesDirectory) { - reject(`Error: provided templatesDirectory "${templatesDirectory}" is not supported. Please make sure to pass something like 'layouts/'`); + if (layoutsDirectory && typeof layoutsDirectory === 'string') { + customConfig.layoutsDirectory = layoutsDirectory; + } else if (layoutsDirectory) { + reject(`Error: provided layoutsDirectory "${layoutsDirectory}" is not supported. Please make sure to pass something like 'layouts/'`); } if (prerender !== undefined) { diff --git a/packages/cli/src/lifecycles/context.js b/packages/cli/src/lifecycles/context.js index d49ae226b..e4f45b564 100644 --- a/packages/cli/src/lifecycles/context.js +++ b/packages/cli/src/lifecycles/context.js @@ -5,17 +5,17 @@ const initContext = async({ config }) => { return new Promise(async (resolve, reject) => { try { - const { workspace, pagesDirectory, templatesDirectory } = config; + const { workspace, pagesDirectory, layoutsDirectory } = config; const projectDirectory = new URL(`file://${process.cwd()}/`); const scratchDir = new URL('./.greenwood/', projectDirectory); const outputDir = new URL('./public/', projectDirectory); const dataDir = new URL('../data/', import.meta.url); - const templatesDir = new URL('../templates/', import.meta.url); + const layoutsDir = new URL('../layouts/', import.meta.url); const userWorkspace = workspace; - const apisDir = new URL('./api/', userWorkspace); const pagesDir = new URL(`./${pagesDirectory}/`, userWorkspace); - const userTemplatesDir = new URL(`./${templatesDirectory}/`, userWorkspace); + const apisDir = new URL('./api/', pagesDir); + const userLayoutsDir = new URL(`./${layoutsDirectory}/`, userWorkspace); const context = { dataDir, @@ -23,10 +23,10 @@ const initContext = async({ config }) => { userWorkspace, apisDir, pagesDir, - userTemplatesDir, + userLayoutsDir, scratchDir, projectDirectory, - templatesDir + layoutsDir }; if (!await checkResourceExists(scratchDir)) { diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js index 57b53b7bc..ad7415d30 100644 --- a/packages/cli/src/lifecycles/graph.js +++ b/packages/cli/src/lifecycles/graph.js @@ -11,7 +11,12 @@ const generateGraph = async (compilation) => { try { const { context, config } = compilation; const { basePath } = config; - const { apisDir, pagesDir, projectDirectory, userWorkspace } = context; + const { pagesDir, projectDirectory, userWorkspace } = context; + const customPageFormatPlugins = config.plugins + .filter(plugin => plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin) + .map(plugin => plugin.provider(compilation)); + + let apis = new Map(); let graph = [{ outputPath: '/index.html', filename: 'index.html', @@ -26,7 +31,7 @@ const generateGraph = async (compilation) => { isolation: false }]; - const walkDirectoryForPages = async function(directory, pages = []) { + const walkDirectoryForPages = async function(directory, pages = [], apiRoutes = new Map()) { const files = await fs.readdir(directory); for (const filename of files) { @@ -35,254 +40,254 @@ const generateGraph = async (compilation) => { const isDirectory = await checkResourceExists(filenameUrlAsDir) && (await fs.stat(filenameUrlAsDir)).isDirectory(); if (isDirectory) { - pages = await walkDirectoryForPages(filenameUrlAsDir, pages); + const nextPages = await walkDirectoryForPages(filenameUrlAsDir, pages, apiRoutes); + + pages = nextPages.pages; + apiRoutes = nextPages.apiRoutes; } else { + const req = new Request(filenameUrl, { headers: { 'Accept': 'text/html' } }); const extension = `.${filenameUrl.pathname.split('.').pop()}`; - const isStatic = extension === '.md' || extension === '.html'; - const isDynamic = extension === '.js'; + const isCustom = customPageFormatPlugins[0] && customPageFormatPlugins[0].shouldServe && await customPageFormatPlugins[0].shouldServe(filenameUrl, req) + ? customPageFormatPlugins[0].servePage + : null; const relativePagePath = filenameUrl.pathname.replace(pagesDir.pathname, '/'); const relativeWorkspacePath = directory.pathname.replace(projectDirectory.pathname, ''); - let route = relativePagePath.replace(extension, ''); - let id = filename.split('/')[filename.split('/').length - 1].replace(extension, ''); - let template = 'page'; - let title = null; - let imports = []; - let customData = {}; - let filePath; - let prerender = true; - let isolation = false; - let hydration = false; - - /* - * check if additional nested directories exist to correctly determine route (minus filename) - * examples: - * - pages/index.{html,md,js} -> / - * - pages/about.{html,md,js} -> /about/ - * - pages/blog/index.{html,md,js} -> /blog/ - * - pages/blog/some-post.{html,md,js} -> /blog/some-post/ - */ - if (relativePagePath.lastIndexOf('/') > 0) { - // https://github.com/ProjectEvergreen/greenwood/issues/455 - route = id === 'index' || route.replace('/index', '') === `/${id}` - ? route.replace('index', '') - : `${route}/`; - } else { - route = route === '/index' - ? '/' - : `${route}/`; - } + const isStatic = isCustom === 'static' || extension === '.md' || extension === '.html'; + const isDynamic = isCustom === 'dynamic' || extension === '.js'; + const isApiRoute = relativePagePath.startsWith('/api'); + const isPage = isStatic || isDynamic; + + if (isApiRoute) { + const req = new Request(filenameUrl); + const extension = filenameUrl.pathname.split('.').pop(); + const isCustom = customPageFormatPlugins[0] && customPageFormatPlugins[0].shouldServe && await customPageFormatPlugins[0].shouldServe(filenameUrl, req); + + if (extension !== 'js' && !isCustom) { + console.warn(`${filenameUrl} is not a supported API file extension, skipping...`); + return; + } - if (isStatic) { - const fileContents = await fs.readFile(filenameUrl, 'utf8'); - const { attributes } = fm(fileContents); - - template = attributes.template || 'page'; - title = attributes.title || title; - id = attributes.label || id; - imports = attributes.imports || []; - filePath = `${relativeWorkspacePath}${filename}`; - - // prune "reserved" attributes that are supported by Greenwood - // https://www.greenwoodjs.io/docs/front-matter - customData = attributes; - - delete customData.label; - delete customData.imports; - delete customData.title; - delete customData.template; - - /* Menu Query - * Custom front matter - Variable Definitions - * -------------------------------------------------- - * menu: the name of the menu in which this item can be listed and queried - * index: the index of this list item within a menu - * linkheadings: flag to tell us where to add page's table of contents as menu items - * tableOfContents: json object containing page's table of contents(list of headings) + const relativeApiPath = filenameUrl.pathname.replace(pagesDir.pathname, '/'); + const route = `${basePath}${relativeApiPath.replace(`.${extension}`, '')}`; + // TODO should this be run in isolation like SSR pages? + // https://github.com/ProjectEvergreen/greenwood/issues/991 + const { isolation } = await import(filenameUrl).then(module => module); + + /* + * API Properties (per route) + *---------------------- + * filename: base filename of the page + * outputPath: the filename to write to when generating a build + * path: path to the file relative to the workspace + * route: URL route for a given page on outputFilePath + * isolation: if this should be run in isolated mode + */ + apiRoutes.set(route, { + filename: filename, + outputPath: `/api/${filename.replace(`.${extension}`, '.js')}`, + path: relativeApiPath, + route, + isolation + }); + } else if (isPage) { + let route = relativePagePath.replace(extension, ''); + let id = filename.split('/')[filename.split('/').length - 1].replace(extension, ''); + let layout = extension === '.html' ? null : 'page'; + let title = null; + let imports = []; + let customData = {}; + let filePath; + let prerender = true; + let isolation = false; + let hydration = false; + + /* + * check if additional nested directories exist to correctly determine route (minus filename) + * examples: + * - pages/index.{html,md,js} -> / + * - pages/about.{html,md,js} -> /about/ + * - pages/blog/index.{html,md,js} -> /blog/ + * - pages/blog/some-post.{html,md,js} -> /blog/some-post/ */ - // set specific menu to place this page - customData.menu = customData.menu || ''; + if (relativePagePath.lastIndexOf('/') > 0) { + // https://github.com/ProjectEvergreen/greenwood/issues/455 + route = id === 'index' || route.replace('/index', '') === `/${id}` + ? route.replace('index', '') + : `${route}/`; + } else { + route = route === '/index' + ? '/' + : `${route}/`; + } - // set specific index list priority of this item within a menu - customData.index = customData.index || ''; + if (isStatic) { + const fileContents = await fs.readFile(filenameUrl, 'utf8'); + const { attributes } = fm(fileContents); - // set flag whether to gather a list of headings on a page as menu items - customData.linkheadings = customData.linkheadings || 0; - customData.tableOfContents = []; + layout = attributes.layout || layout; + title = attributes.title || title; + id = attributes.label || id; + imports = attributes.imports || []; + filePath = `${relativeWorkspacePath}${filename}`; - if (customData.linkheadings > 0) { - // parse markdown for table of contents and output to json - customData.tableOfContents = toc(fileContents).json; - customData.tableOfContents.shift(); + // prune "reserved" attributes that are supported by Greenwood + // https://www.greenwoodjs.io/docs/front-matter + customData = attributes; - // parse table of contents for only the pages user wants linked - if (customData.tableOfContents.length > 0 && customData.linkheadings > 0) { - customData.tableOfContents = customData.tableOfContents - .filter((item) => item.lvl === customData.linkheadings); - } - } - /* ---------End Menu Query-------------------- */ - } else if (isDynamic) { - const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation).executeModuleUrl; - let ssrFrontmatter; - - filePath = route; - - 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 ?? false; - isolation = result.isolation ?? isolation; - hydration = result.hydration ?? hydration; - - if (result.frontmatter) { - result.frontmatter.imports = result.frontmatter.imports || []; - ssrFrontmatter = result.frontmatter; - } + delete customData.label; + delete customData.imports; + delete customData.title; + delete customData.layout; - resolve(); - }); - worker.on('error', reject); - worker.on('exit', (code) => { - if (code !== 0) { - reject(new Error(`Worker stopped with exit code ${code}`)); + /* Menu Query + * Custom front matter - Variable Definitions + * -------------------------------------------------- + * menu: the name of the menu in which this item can be listed and queried + * index: the index of this list item within a menu + * linkheadings: flag to tell us where to add page's table of contents as menu items + * tableOfContents: json object containing page's table of contents(list of headings) + */ + // set specific menu to place this page + customData.menu = customData.menu || ''; + + // set specific index list priority of this item within a menu + customData.index = customData.index || ''; + + // set flag whether to gather a list of headings on a page as menu items + customData.linkheadings = customData.linkheadings || 0; + customData.tableOfContents = []; + + if (customData.linkheadings > 0) { + // parse markdown for table of contents and output to json + customData.tableOfContents = toc(fileContents).json; + customData.tableOfContents.shift(); + + // parse table of contents for only the pages user wants linked + if (customData.tableOfContents.length > 0 && customData.linkheadings > 0) { + customData.tableOfContents = customData.tableOfContents + .filter((item) => item.lvl === customData.linkheadings); } + } + /* ---------End Menu Query-------------------- */ + } else if (isDynamic) { + const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation).executeModuleUrl; + let ssrFrontmatter; + + filePath = route; + + await new Promise(async (resolve, reject) => { + const worker = new Worker(new URL('../lib/ssr-route-worker.js', import.meta.url)); + const request = await requestAsObject(new Request(filenameUrl)); + + worker.on('message', async (result) => { + prerender = result.prerender ?? false; + isolation = result.isolation ?? isolation; + hydration = result.hydration ?? hydration; + + if (result.frontmatter) { + result.frontmatter.imports = result.frontmatter.imports || []; + ssrFrontmatter = result.frontmatter; + } + + resolve(); + }); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Worker stopped with exit code ${code}`)); + } + }); + + worker.postMessage({ + executeModuleUrl: routeWorkerUrl.href, + moduleUrl: filenameUrl.href, + compilation: JSON.stringify(compilation), + // TODO need to get as many of these params as possible + // or ignore completely? + page: JSON.stringify({ + servePage: isCustom, + route, + id, + label: id.split('-') + .map((idPart) => { + return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; + }).join(' ') + }), + request + }); }); - worker.postMessage({ - executeModuleUrl: routeWorkerUrl.href, - moduleUrl: filenameUrl.href, - compilation: JSON.stringify(compilation), - // 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(' ') - }), - request - }); - }); - - if (ssrFrontmatter) { - template = ssrFrontmatter.template || template; - title = ssrFrontmatter.title || title; - imports = ssrFrontmatter.imports || imports; - customData = ssrFrontmatter.data || customData; - - /* Menu Query - * Custom front matter - Variable Definitions - * -------------------------------------------------- - * menu: the name of the menu in which this item can be listed and queried - * index: the index of this list item within a menu - * linkheadings: flag to tell us where to add page's table of contents as menu items - * tableOfContents: json object containing page's table of contents(list of headings) - */ - customData.menu = ssrFrontmatter.menu || ''; - customData.index = ssrFrontmatter.index || ''; + if (ssrFrontmatter) { + layout = ssrFrontmatter.layout || layout; + title = ssrFrontmatter.title || title; + imports = ssrFrontmatter.imports || imports; + customData = ssrFrontmatter.data || customData; + + /* Menu Query + * Custom front matter - Variable Definitions + * -------------------------------------------------- + * menu: the name of the menu in which this item can be listed and queried + * index: the index of this list item within a menu + * linkheadings: flag to tell us where to add page's table of contents as menu items + * tableOfContents: json object containing page's table of contents(list of headings) + */ + customData.menu = ssrFrontmatter.menu || ''; + customData.index = ssrFrontmatter.index || ''; + } } - } else { - console.debug(`Unhandled extension (.${extension}) for route => ${route}`); - } - /* - * Graph Properties (per page) - *---------------------- - * 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 - * outputPath: the filename to write to when generating static HTML - * path: path to the file relative to the workspace - * 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 - * prerender: if this should be statically exported - * isolation: if this should be run in isolated mode - * hydration: if this page needs hydration support - */ - pages.push({ - data: customData || {}, - filename, - id, - relativeWorkspacePagePath: relativePagePath, - label: id.split('-') - .map((idPart) => { - return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; - }).join(' '), - imports, - resources: [], - outputPath: route === '/404/' - ? '/404.html' - : `${route}index.html`, - path: filePath, - route: `${basePath}${route}`, - template, - title, - isSSR: !isStatic, - prerender, - isolation, - hydration - }); - } - } - - return pages; - }; - - const walkDirectoryForApis = async function(directory, apis = new Map()) { - const files = await fs.readdir(directory); - - for (const filename of files) { - const filenameUrl = new URL(`./${filename}`, directory); - const filenameUrlAsDir = new URL(`./${filename}/`, directory); - const isDirectory = await checkResourceExists(filenameUrlAsDir) && (await fs.stat(filenameUrlAsDir)).isDirectory(); - - if (isDirectory) { - apis = await walkDirectoryForApis(filenameUrlAsDir, apis); - } else { - const extension = filenameUrl.pathname.split('.').pop(); - - if (extension !== 'js') { - console.warn(`${filenameUrl} is not a JavaScript file, skipping...`); - return; + /* + * Graph Properties (per page) + *---------------------- + * 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 + * outputPath: the filename to write to when generating static HTML + * path: path to the file relative to the workspace + * route: URL route for a given page on outputFilePath + * layout: page layout 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 + * prerender: if this should be statically exported + * isolation: if this should be run in isolated mode + * hydration: if this page needs hydration support + * servePage: signal that this is a custom page file type (static | dynamic) + */ + pages.push({ + data: customData || {}, + filename, + id, + relativeWorkspacePagePath: relativePagePath, + label: id.split('-') + .map((idPart) => { + return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; + }).join(' '), + imports, + resources: [], + outputPath: route === '/404/' + ? '/404.html' + : `${route}index.html`, + path: filePath, + route: `${basePath}${route}`, + layout, + title, + isSSR: !isStatic, + prerender, + isolation, + hydration, + servePage: isCustom + }); + } else { + console.debug(`Unhandled extension (${extension}) for route => ${route}`); } - - const relativeApiPath = filenameUrl.pathname.replace(userWorkspace.pathname, '/'); - const route = `${basePath}${relativeApiPath.replace(`.${extension}`, '')}`; - // TODO should this be run in isolation like SSR pages? - // https://github.com/ProjectEvergreen/greenwood/issues/991 - const { isolation } = await import(filenameUrl).then(module => module); - - /* - * API Properties (per route) - *---------------------- - * filename: base filename of the page - * outputPath: the filename to write to when generating a build - * path: path to the file relative to the workspace - * route: URL route for a given page on outputFilePath - * isolation: if this should be run in isolated mode - */ - apis.set(route, { - filename: filename, - outputPath: `/api/${filename}`, - path: relativeApiPath, - route, - isolation - }); } } - return apis; + return { pages, apiRoutes }; }; console.debug('building from local sources...'); @@ -296,8 +301,10 @@ const generateGraph = async (compilation) => { }]; } else { const oldGraph = graph[0]; + const pages = await checkResourceExists(pagesDir) ? await walkDirectoryForPages(pagesDir) : { pages: graph, apiRoutes: apis }; - graph = await checkResourceExists(pagesDir) ? await walkDirectoryForPages(pagesDir) : graph; + graph = pages.pages; + apis = pages.apiRoutes; const has404Page = graph.find(page => page.route.endsWith('/404/')); @@ -353,12 +360,7 @@ const generateGraph = async (compilation) => { } compilation.graph = graph; - - if (await checkResourceExists(apisDir)) { - const apis = await walkDirectoryForApis(apisDir); - - compilation.manifest = { apis }; - } + compilation.manifest = { apis }; resolve(compilation); } catch (err) { diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 804cfd4a1..04007e00e 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -3,8 +3,6 @@ import { checkResourceExists, trackResourcesForRoute } from '../lib/resource-uti import os from 'os'; 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.endsWith('/404/') && !await checkResourceExists(outputDir)) { await fs.mkdir(outputDir, { @@ -62,18 +60,32 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { 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; + const { route, outputPath } = page; const outputPathUrl = new URL(`.${outputPath}`, scratchDir); const url = new URL(`http://localhost:${compilation.config.port}${route}`); const request = new Request(url); + let ssrContents; + // do we negate the worker pool by also running this, outside the pool? let body = await (await servePage(url, request, plugins)).text(); body = await (await interceptPage(url, request, plugins, body)).text(); - await createOutputDirectory(route, new URL(outputPathUrl.href.replace('index.html', ''))); + // hack to avoid over-rendering SSR content + // https://github.com/ProjectEvergreen/greenwood/issues/1044 + // https://github.com/ProjectEvergreen/greenwood/issues/988#issuecomment-1288168858 + if (page.isSSR) { + const ssrContentsMatch = /(.*.)/s; + + ssrContents = body.match(ssrContentsMatch)[0]; + body = body.replace(ssrContents, ''); + + ssrContents = ssrContents + .replace('', '') + .replace('', ''); + } + const resources = await trackResourcesForRoute(body, compilation, route); const scripts = resources - .map(resource => compilation.resources.get(resource)) .filter(resource => resource.type === 'script') .map(resource => resource.sourcePathURL.href); @@ -95,6 +107,11 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { }); }); + if (page.isSSR) { + body = body.replace('', ssrContents); + } + + await createOutputDirectory(route, new URL(outputPathUrl.href.replace('index.html', ''))); await fs.writeFile(outputPathUrl, body); console.info('generated page...', route); @@ -117,7 +134,7 @@ async function preRenderCompilationCustom(compilation, customPrerender) { body = body.replace(/ `) diff --git a/packages/cli/test/cases/build.config.error-templates-directory/build.config.error-templates-directory.spec.js b/packages/cli/test/cases/build.config.error-layouts-directory/build.config.error-layouts-directory.spec.js similarity index 63% rename from packages/cli/test/cases/build.config.error-templates-directory/build.config.error-templates-directory.spec.js rename to packages/cli/test/cases/build.config.error-layouts-directory/build.config.error-layouts-directory.spec.js index 8b481fd00..e48c2130a 100644 --- a/packages/cli/test/cases/build.config.error-templates-directory/build.config.error-templates-directory.spec.js +++ b/packages/cli/test/cases/build.config.error-layouts-directory/build.config.error-layouts-directory.spec.js @@ -1,6 +1,6 @@ /* * Use Case - * Run Greenwood build command with a bad value for templatesDirectory in a custom config. + * Run Greenwood build command with a bad value for layoutsDirectory in a custom config. * * User Result * Should throw an error. @@ -10,7 +10,7 @@ * * User Config * { - * templatesDirectory: {} + * layoutsDirectory: {} * } * * User Workspace @@ -35,13 +35,13 @@ describe('Build Greenwood With: ', function() { runner = new Runner(); }); - describe('Custom Configuration with a bad value for templatesDirectory', function() { - it('should throw an error that templatesDirectory must be a string', function() { + describe('Custom Configuration with a bad value for layoutsDirectory', function() { + it('should throw an error that layoutsDirectory must be a string', async function() { try { runner.setup(outputPath); runner.runCommand(cliPath, 'build'); } catch (err) { - expect(err).to.contain('Error: provided templatesDirectory "[object Object]" is not supported. Please make sure to pass something like \'layouts/\''); + expect(err).to.contain('Error: provided layoutsDirectory "[object Object]" is not supported. Please make sure to pass something like \'layouts/\''); } }); }); diff --git a/packages/cli/test/cases/build.config.error-layouts-directory/greenwood.config.js b/packages/cli/test/cases/build.config.error-layouts-directory/greenwood.config.js new file mode 100644 index 000000000..376ab3249 --- /dev/null +++ b/packages/cli/test/cases/build.config.error-layouts-directory/greenwood.config.js @@ -0,0 +1,3 @@ +export default { + layoutsDirectory: {} +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.error-templates-directory/greenwood.config.js b/packages/cli/test/cases/build.config.error-templates-directory/greenwood.config.js deleted file mode 100644 index b8e6daaba..000000000 --- a/packages/cli/test/cases/build.config.error-templates-directory/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - templatesDirectory: {} -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js b/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js index 62b30ae9c..b39193067 100644 --- a/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js +++ b/packages/cli/test/cases/build.config.interpolate-frontmatter/build.config.interpolate-frontmatter.spec.js @@ -19,7 +19,7 @@ * pages/ * blog/ * first-post.md - * templates/ + * layouts/ * blog.html */ import { JSDOM } from 'jsdom'; diff --git a/packages/cli/test/cases/build.config.interpolate-frontmatter/src/templates/blog.html b/packages/cli/test/cases/build.config.interpolate-frontmatter/src/layouts/blog.html similarity index 100% rename from packages/cli/test/cases/build.config.interpolate-frontmatter/src/templates/blog.html rename to packages/cli/test/cases/build.config.interpolate-frontmatter/src/layouts/blog.html diff --git a/packages/cli/test/cases/build.config.interpolate-frontmatter/src/pages/blog/first-post.md b/packages/cli/test/cases/build.config.interpolate-frontmatter/src/pages/blog/first-post.md index deae1bb80..fa488585c 100644 --- a/packages/cli/test/cases/build.config.interpolate-frontmatter/src/pages/blog/first-post.md +++ b/packages/cli/test/cases/build.config.interpolate-frontmatter/src/pages/blog/first-post.md @@ -1,6 +1,6 @@ --- title: Ny First Post -template: blog +layout: blog published: 11/11/2022 author: Owen Buckley --- diff --git a/packages/cli/test/cases/build.config.templates-directory/build.config.templates-directory.spec.js b/packages/cli/test/cases/build.config.layouts-directory/build.config.layouts-directory.spec.js similarity index 89% rename from packages/cli/test/cases/build.config.templates-directory/build.config.templates-directory.spec.js rename to packages/cli/test/cases/build.config.layouts-directory/build.config.layouts-directory.spec.js index baac1c79b..f206cf923 100644 --- a/packages/cli/test/cases/build.config.templates-directory/build.config.templates-directory.spec.js +++ b/packages/cli/test/cases/build.config.layouts-directory/build.config.layouts-directory.spec.js @@ -1,6 +1,6 @@ /* * Use Case - * Run Greenwood with a custom name for templates directory. + * Run Greenwood with a custom name for layouts directory. * * User Result * Should generate a bare bones Greenwood build. (same as build.default.spec.js) with custom title in header @@ -10,7 +10,7 @@ * * User Config * { - * templatesDirectory: 'layouts' + * layoutsDirectory: 'layouts' * } * * User Workspace @@ -68,7 +68,7 @@ describe('Build Greenwood With: ', function() { it('should have the correct page heading', function() { const heading = dom.window.document.querySelectorAll('head title')[0].textContent; - expect(heading).to.be.equal('Custom Layout Page Template'); + expect(heading).to.be.equal('Custom Layout Page Layout'); }); it('should have the correct page heading', function() { @@ -80,7 +80,7 @@ describe('Build Greenwood With: ', function() { it('should have the correct page heading', function() { const paragraph = dom.window.document.querySelectorAll('body p')[0].textContent; - expect(paragraph).to.be.equal('A page using a page template from a custom layout directory.'); + expect(paragraph).to.be.equal('A page using a page layout from a custom layout directory.'); }); }); }); diff --git a/packages/cli/test/cases/build.config.layouts-directory/greenwood.config.js b/packages/cli/test/cases/build.config.layouts-directory/greenwood.config.js new file mode 100644 index 000000000..04634f17c --- /dev/null +++ b/packages/cli/test/cases/build.config.layouts-directory/greenwood.config.js @@ -0,0 +1,3 @@ +export default { + layoutsDirectory: 'my-layouts' +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.templates-directory/src/layouts/page.html b/packages/cli/test/cases/build.config.layouts-directory/src/my-layouts/page.html similarity index 66% rename from packages/cli/test/cases/build.config.templates-directory/src/layouts/page.html rename to packages/cli/test/cases/build.config.layouts-directory/src/my-layouts/page.html index b7fe92d12..0fc6a16f9 100644 --- a/packages/cli/test/cases/build.config.templates-directory/src/layouts/page.html +++ b/packages/cli/test/cases/build.config.layouts-directory/src/my-layouts/page.html @@ -1,6 +1,6 @@ - Custom Layout Page Template + Custom Layout Page Layout diff --git a/packages/cli/test/cases/build.config.layouts-directory/src/pages/index.md b/packages/cli/test/cases/build.config.layouts-directory/src/pages/index.md new file mode 100644 index 000000000..3332d3216 --- /dev/null +++ b/packages/cli/test/cases/build.config.layouts-directory/src/pages/index.md @@ -0,0 +1,3 @@ +# Home Page + +A page using a page layout from a custom layout directory. \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js b/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js index 149ae06dd..f025f762b 100644 --- a/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js +++ b/packages/cli/test/cases/build.config.optimization-inline/build.config-optimization-inline.spec.js @@ -18,6 +18,8 @@ * components/ * foobar.js * header.js + * layouts/ + * app.html * pages/ * index.html * styles/ diff --git a/packages/cli/test/cases/build.config.optimization-inline/src/templates/app.html b/packages/cli/test/cases/build.config.optimization-inline/src/layouts/app.html similarity index 100% rename from packages/cli/test/cases/build.config.optimization-inline/src/templates/app.html rename to packages/cli/test/cases/build.config.optimization-inline/src/layouts/app.html diff --git a/packages/cli/test/cases/build.config.prerender/build.config.prerender.spec.js b/packages/cli/test/cases/build.config.prerender/build.config.prerender.spec.js index 6a86102e6..881d19886 100644 --- a/packages/cli/test/cases/build.config.prerender/build.config.prerender.spec.js +++ b/packages/cli/test/cases/build.config.prerender/build.config.prerender.spec.js @@ -98,12 +98,6 @@ describe('Build Greenwood With: ', function() { }); }); - it('should have the expected heading text within the index page in the public directory', function() { - const heading = dom.window.document.querySelector('body h1').textContent; - - expect(heading).to.equal('Welcome to Greenwood!'); - }); - it('should have prerendered content from component', function() { const appHeader = dom.window.document.querySelectorAll('body app-header'); const header = dom.window.document.querySelectorAll('body header'); diff --git a/packages/cli/test/cases/build.config.static-router/build.config.static-router.spec.js b/packages/cli/test/cases/build.config.static-router/build.config.static-router.spec.js index 56a992c28..f90f340a3 100644 --- a/packages/cli/test/cases/build.config.static-router/build.config.static-router.spec.js +++ b/packages/cli/test/cases/build.config.static-router/build.config.static-router.spec.js @@ -95,7 +95,7 @@ describe('Build Greenwood With: ', function() { expect(inlineRouterTags.length).to.be.equal(1); expect(inlineRouterTags[0].textContent).to.contain('window.__greenwood = window.__greenwood || {};'); - expect(inlineRouterTags[0].textContent).to.contain('window.__greenwood.currentTemplate = "page"'); + expect(inlineRouterTags[0].textContent).to.contain('window.__greenwood.currentLayout = "page"'); }); it('should have one tag in the for the content', function() { @@ -117,7 +117,7 @@ describe('Build Greenwood With: ', function() { const dataset = aboutRouteTag[0].dataset; expect(aboutRouteTag.length).to.be.equal(1); - expect(dataset.template).to.be.equal('test'); + expect(dataset.layout).to.be.equal('test'); expect(dataset.key).to.be.equal('/_routes/about/index.html'); }); @@ -128,7 +128,7 @@ describe('Build Greenwood With: ', function() { const dataset = aboutRouteTag[0].dataset; expect(aboutRouteTag.length).to.be.equal(1); - expect(dataset.template).to.be.equal('page'); + expect(dataset.layout).to.be.equal('page'); expect(dataset.key).to.be.equal('/_routes/index.html'); }); diff --git a/packages/cli/test/cases/build.config.static-router/src/pages/about.md b/packages/cli/test/cases/build.config.static-router/src/pages/about.md index b90299ace..5618aaa66 100644 --- a/packages/cli/test/cases/build.config.static-router/src/pages/about.md +++ b/packages/cli/test/cases/build.config.static-router/src/pages/about.md @@ -1,5 +1,5 @@ --- -template: test +layout: test --- ### Greenwood diff --git a/packages/cli/test/cases/build.config.static-router/src/pages/regex-test.html b/packages/cli/test/cases/build.config.static-router/src/pages/regex-test.html index e25174ef9..70b2aba4e 100644 --- a/packages/cli/test/cases/build.config.static-router/src/pages/regex-test.html +++ b/packages/cli/test/cases/build.config.static-router/src/pages/regex-test.html @@ -176,7 +176,7 @@

The static site generator for your. . .
-
+

Greenwood is a modern and performant static site generator for Web Component based development.

diff --git a/packages/cli/test/cases/build.config.templates-directory/greenwood.config.js b/packages/cli/test/cases/build.config.templates-directory/greenwood.config.js deleted file mode 100644 index 4ea248e54..000000000 --- a/packages/cli/test/cases/build.config.templates-directory/greenwood.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - templatesDirectory: 'layouts' -}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.templates-directory/src/pages/index.md b/packages/cli/test/cases/build.config.templates-directory/src/pages/index.md deleted file mode 100644 index a44ed2d17..000000000 --- a/packages/cli/test/cases/build.config.templates-directory/src/pages/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Home Page - -A page using a page template from a custom layout directory. \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.meta/build.default.meta.spec.js b/packages/cli/test/cases/build.default.meta/build.default.meta.spec.js index 23efa5a6d..5dae7737f 100644 --- a/packages/cli/test/cases/build.default.meta/build.default.meta.spec.js +++ b/packages/cli/test/cases/build.default.meta/build.default.meta.spec.js @@ -1,6 +1,6 @@ /* * Use Case - * Run Greenwood and tests for correct `` tag merging for pages and templates. + * Run Greenwood and tests for correct `` tag merging for pages and layouts. * * User Result * Should generate a bare bones Greenwood build with one nested About page with expected meta values. @@ -19,7 +19,7 @@ * index.md * hello.md * index.md - * template/ + * layout/ * app.html * page.html */ diff --git a/packages/cli/test/cases/build.default.meta/src/templates/page.html b/packages/cli/test/cases/build.default.meta/src/layouts/page.html similarity index 100% rename from packages/cli/test/cases/build.default.meta/src/templates/page.html rename to packages/cli/test/cases/build.default.meta/src/layouts/page.html diff --git a/packages/cli/test/cases/build.default.ssr-prerender/build.default.ssr-prerender.spec.js b/packages/cli/test/cases/build.default.ssr-prerender/build.default.ssr-prerender.spec.js index f4eb9413e..001fd39a8 100644 --- a/packages/cli/test/cases/build.default.ssr-prerender/build.default.ssr-prerender.spec.js +++ b/packages/cli/test/cases/build.default.ssr-prerender/build.default.ssr-prerender.spec.js @@ -19,7 +19,7 @@ * footer.js * pages/ * index.js - * templates/ + * layouts/ * app.html */ import chai from 'chai'; @@ -68,17 +68,35 @@ describe('Build Greenwood With: ', function() { expect(headings[0].textContent).to.equal('This is the home page.'); }); - it('should have one top level element with a