From b420c5c3a4b1bfff8e96c639b2cbc488e869cbe3 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 29 Apr 2024 15:33:39 -0400 Subject: [PATCH] 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 --- .eslintrc.cjs | 23 +- .../workflows/{ci-exp.yml => ci-loaders.yml} | 4 +- .../{ci-win-exp.yml => ci-win-loaders.yml} | 4 +- .gitignore | 1 + .ls-lint.yml | 1 + .nvmrc | 2 +- greenwood.config.js | 11 +- package.json | 16 +- packages/cli/package.json | 6 +- packages/cli/src/commands/build.js | 4 + packages/cli/src/config/rollup.config.js | 75 ++- packages/cli/src/lib/resource-utils.js | 11 +- packages/cli/src/lifecycles/bundle.js | 25 +- packages/cli/src/lifecycles/prerender.js | 4 + packages/cli/src/lifecycles/serve.js | 36 ++ packages/cli/src/loader.js | 49 +- .../plugins/resource/plugin-node-modules.js | 7 +- .../plugins/resource/plugin-standard-css.js | 23 +- .../plugins/resource/plugin-standard-json.js | 18 + .../theme-pack-context-plugin.js | 13 +- .../greenwood.config.js | 4 +- .../loaders-build.import-attributes.spec.js | 77 +++ .../src/components/card/card.css | 3 + .../src/components/card/card.js | 27 ++ .../src/components/card/card.json | 5 + .../src/pages/index.html | 11 + .../greenwood.config.js | 3 + ...-build.prerender-import-attributes.spec.js | 97 ++++ .../package.json | 3 + .../src/components/hero/hero.css | 18 + .../src/components/hero/hero.js | 40 ++ .../src/components/hero/hero.json | 3 + .../src/index.html | 12 + ...ders-develop.ssr-import-attributes.spec.js | 186 +++++++ .../src/api/fragment.js | 17 + .../src/components/card/card.css | 3 + .../src/components/card/card.js | 27 ++ .../src/components/card/card.json | 5 + .../src/pages/greeting.js | 10 + ...erve.default.ssr-import-attributes.spec.js | 167 +++++++ .../src/api/fragment.js | 17 + .../src/components/card/card.css | 3 + .../src/components/card/card.js | 27 ++ .../src/components/card/card.json | 5 + .../src/pages/greeting.js | 10 + packages/plugin-babel/src/index.js | 4 +- packages/plugin-graphql/README.md | 4 +- .../greenwood.config.js | 0 .../loaders-prerender.query-children.spec.js} | 0 .../package.json | 0 .../src/components/posts-list.js | 2 +- .../src/pages/blog/first-post/index.md | 0 .../src/pages/blog/second-post/index.md | 0 .../src/pages/index.html | 0 packages/plugin-import-css/README.md | 2 + packages/plugin-import-css/package.json | 4 +- .../test/cases/default/default.spec.js | 2 +- .../develop.default/develop.default.spec.js | 2 +- .../exp-build.prerender.spec.js | 2 +- .../cases/exp-serve.ssr/exp-serve.ssr.spec.js | 2 +- packages/plugin-import-json/README.md | 2 + packages/plugin-import-json/package.json | 4 +- .../test/cases/default/default.spec.js | 2 +- .../develop.default/develop.default.spec.js | 2 +- .../exp-build.prerender.spec.js | 2 +- .../cases/exp-serve.ssr/exp-serve.ssr.spec.js | 2 +- packages/plugin-import-jsx/README.md | 11 +- packages/plugin-import-jsx/package.json | 2 +- packages/plugin-import-jsx/src/index.js | 7 +- .../test/cases/default/src/templates/app.html | 2 +- .../greenwood.config.js | 0 .../loaders-build.prerender.spec.js} | 3 +- .../package.json | 0 .../src/components/footer.jsx | 0 .../cases/loaders-build.prerender/src/main.js | 1 + .../src/pages/index.md | 0 .../src/templates/app.html | 2 +- packages/plugin-import-raw/README.md | 63 +++ packages/plugin-import-raw/package.json | 31 ++ packages/plugin-import-raw/src/index.js | 67 +++ .../build.matchers/build.matchers.spec.js | 84 ++++ .../cases/build.matchers/greenwood.config.js | 12 + .../test/cases/build.matchers/src/main.js | 3 + .../cases/build.matchers/src/pages/index.html | 12 + .../test/cases/default/default.spec.js | 83 ++++ .../test/cases/default/greenwood.config.js | 8 + .../test/cases/default/src/main.js | 3 + .../test/cases/default/src/pages/index.html | 12 + .../test/cases/default/src/styles.css | 3 + .../develop.default/develop.default.spec.js | 126 +++++ .../cases/develop.default/greenwood.config.js | 7 + .../test/cases/develop.default/package.json | 4 + .../test/cases/develop.default/src/main.css | 12 + .../cases/develop.default/src/styles.css.js | 1 + .../greenwood.config.js | 8 + .../loaders-build.prerender.spec.js | 90 ++++ .../loaders-build.prerender/package.json | 4 + .../src/components/footer.css | 1 + .../src/components/footer.js | 25 + .../src/pages/index.md | 3 + .../src/templates/app.html | 12 + .../loaders-serve.ssr/greenwood.config.js | 7 + .../loaders-serve.ssr.spec.js | 152 ++++++ .../test/cases/loaders-serve.ssr/package.json | 4 + .../loaders-serve.ssr/src/api/fragment.js | 28 ++ .../loaders-serve.ssr/src/components/card.css | 44 ++ .../loaders-serve.ssr/src/components/card.js | 31 ++ .../loaders-serve.ssr/src/pages/products.js | 31 ++ .../src/services/products.js | 11 + .../loaders-serve.ssr/src/styles/some.css | 3 + packages/plugin-postcss/README.md | 2 - packages/plugin-postcss/src/index.js | 8 +- packages/plugin-renderer-lit/README.md | 5 +- .../build.prerender.getting-started.spec.js | 6 +- .../greenwood.config.js | 9 + ...-build.prerender.import-attributes.spec.js | 193 ++++++++ .../package.json | 7 + .../src/components/header/header.css | 5 + .../src/components/header/header.js | 27 ++ .../src/components/header/nav.json | 3 + .../src/pages/index.html | 19 + .../cases/serve.default/serve.default.spec.js | 10 +- .../greenwood.config.js | 0 .../loaders-serve.prerender-ssr.spec.js} | 0 .../package.json | 0 .../src/components/card/card.ts | 0 .../src/pages/index.html | 0 .../greenwood.config.js | 0 .../loaders-serve.ssr.spec.js} | 0 .../package.json | 0 .../src/api/fragment.js | 0 .../src/components/card/card.ts | 0 .../src/components/card/logo.png | Bin .../src/components/card/styles.ts | 0 .../src/pages/index.html | 0 tsconfig.json | 5 +- www/components/banner/banner.js | 4 +- www/components/banner/eve-button.js | 2 +- www/components/card/card.js | 2 +- www/components/header/header.js | 2 +- www/components/shelf/shelf.js | 2 +- www/components/social-icons/social-icons.js | 2 +- www/pages/docs/scripts.md | 23 +- www/pages/docs/server-rendering.md | 25 +- www/pages/plugins/custom-plugins.md | 3 +- www/pages/plugins/resource.md | 94 ++-- yarn.lock | 457 +++++++++++++----- 147 files changed, 2777 insertions(+), 302 deletions(-) rename .github/workflows/{ci-exp.yml => ci-loaders.yml} (89%) rename .github/workflows/{ci-win-exp.yml => ci-win-loaders.yml} (85%) create mode 100644 packages/cli/test/cases/loaders-build.import-attributes/loaders-build.import-attributes.spec.js create mode 100644 packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.css create mode 100644 packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.js create mode 100644 packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.json create mode 100644 packages/cli/test/cases/loaders-build.import-attributes/src/pages/index.html create mode 100644 packages/cli/test/cases/loaders-build.prerender-import-attributes/greenwood.config.js create mode 100644 packages/cli/test/cases/loaders-build.prerender-import-attributes/loaders-build.prerender-import-attributes.spec.js create mode 100644 packages/cli/test/cases/loaders-build.prerender-import-attributes/package.json create mode 100644 packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.css create mode 100644 packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.js create mode 100644 packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.json create mode 100644 packages/cli/test/cases/loaders-build.prerender-import-attributes/src/index.html create mode 100644 packages/cli/test/cases/loaders-develop.ssr-import-attributes/loaders-develop.ssr-import-attributes.spec.js create mode 100644 packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/api/fragment.js create mode 100644 packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.css create mode 100644 packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.js create mode 100644 packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.json create mode 100644 packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/pages/greeting.js create mode 100644 packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/loaders-serve.default.ssr-import-attributes.spec.js create mode 100644 packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/api/fragment.js create mode 100644 packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.css create mode 100644 packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.js create mode 100644 packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.json create mode 100644 packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/pages/greeting.js rename packages/plugin-graphql/test/cases/{exp-prerender.query-children => loaders-prerender.query-children}/greenwood.config.js (100%) rename packages/plugin-graphql/test/cases/{exp-prerender.query-children/exp-prerender.query-children.spec.js => loaders-prerender.query-children/loaders-prerender.query-children.spec.js} (100%) rename packages/plugin-graphql/test/cases/{exp-prerender.query-children => loaders-prerender.query-children}/package.json (100%) rename packages/plugin-graphql/test/cases/{exp-prerender.query-children => loaders-prerender.query-children}/src/components/posts-list.js (95%) rename packages/plugin-graphql/test/cases/{exp-prerender.query-children => loaders-prerender.query-children}/src/pages/blog/first-post/index.md (100%) rename packages/plugin-graphql/test/cases/{exp-prerender.query-children => loaders-prerender.query-children}/src/pages/blog/second-post/index.md (100%) rename packages/plugin-graphql/test/cases/{exp-prerender.query-children => loaders-prerender.query-children}/src/pages/index.html (100%) rename packages/plugin-import-jsx/test/cases/{exp-build.prerender => loaders-build.prerender}/greenwood.config.js (100%) rename packages/plugin-import-jsx/test/cases/{exp-build.prerender/exp-build.prerender.spec.js => loaders-build.prerender/loaders-build.prerender.spec.js} (97%) rename packages/plugin-import-jsx/test/cases/{exp-build.prerender => loaders-build.prerender}/package.json (100%) rename packages/plugin-import-jsx/test/cases/{exp-build.prerender => loaders-build.prerender}/src/components/footer.jsx (100%) create mode 100644 packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/main.js rename packages/plugin-import-jsx/test/cases/{exp-build.prerender => loaders-build.prerender}/src/pages/index.md (100%) rename packages/plugin-import-jsx/test/cases/{exp-build.prerender => loaders-build.prerender}/src/templates/app.html (61%) create mode 100644 packages/plugin-import-raw/README.md create mode 100644 packages/plugin-import-raw/package.json create mode 100644 packages/plugin-import-raw/src/index.js create mode 100644 packages/plugin-import-raw/test/cases/build.matchers/build.matchers.spec.js create mode 100644 packages/plugin-import-raw/test/cases/build.matchers/greenwood.config.js create mode 100644 packages/plugin-import-raw/test/cases/build.matchers/src/main.js create mode 100644 packages/plugin-import-raw/test/cases/build.matchers/src/pages/index.html create mode 100644 packages/plugin-import-raw/test/cases/default/default.spec.js create mode 100644 packages/plugin-import-raw/test/cases/default/greenwood.config.js create mode 100644 packages/plugin-import-raw/test/cases/default/src/main.js create mode 100644 packages/plugin-import-raw/test/cases/default/src/pages/index.html create mode 100644 packages/plugin-import-raw/test/cases/default/src/styles.css create mode 100644 packages/plugin-import-raw/test/cases/develop.default/develop.default.spec.js create mode 100644 packages/plugin-import-raw/test/cases/develop.default/greenwood.config.js create mode 100644 packages/plugin-import-raw/test/cases/develop.default/package.json create mode 100644 packages/plugin-import-raw/test/cases/develop.default/src/main.css create mode 100644 packages/plugin-import-raw/test/cases/develop.default/src/styles.css.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-build.prerender/greenwood.config.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-build.prerender/loaders-build.prerender.spec.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-build.prerender/package.json create mode 100644 packages/plugin-import-raw/test/cases/loaders-build.prerender/src/components/footer.css create mode 100644 packages/plugin-import-raw/test/cases/loaders-build.prerender/src/components/footer.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-build.prerender/src/pages/index.md create mode 100644 packages/plugin-import-raw/test/cases/loaders-build.prerender/src/templates/app.html create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/greenwood.config.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/loaders-serve.ssr.spec.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/package.json create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/api/fragment.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/components/card.css create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/components/card.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/pages/products.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/services/products.js create mode 100644 packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/styles/some.css create mode 100644 packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/greenwood.config.js create mode 100644 packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/loaders-build.prerender.import-attributes.spec.js create mode 100644 packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/package.json create mode 100644 packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/src/components/header/header.css create mode 100644 packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/src/components/header/header.js create mode 100644 packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/src/components/header/nav.json create mode 100644 packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/src/pages/index.html rename packages/plugin-typescript/test/cases/{exp-prerender.serve.ssr => loaders-serve.prerender-ssr}/greenwood.config.js (100%) rename packages/plugin-typescript/test/cases/{exp-prerender.serve.ssr/exp-prerender.serve.ssr.spec.js => loaders-serve.prerender-ssr/loaders-serve.prerender-ssr.spec.js} (100%) rename packages/plugin-typescript/test/cases/{exp-prerender.serve.ssr => loaders-serve.prerender-ssr}/package.json (100%) rename packages/plugin-typescript/test/cases/{exp-prerender.serve.ssr => loaders-serve.prerender-ssr}/src/components/card/card.ts (100%) rename packages/plugin-typescript/test/cases/{exp-prerender.serve.ssr => loaders-serve.prerender-ssr}/src/pages/index.html (100%) rename packages/plugin-typescript/test/cases/{exp-serve.ssr => loaders-serve.ssr}/greenwood.config.js (100%) rename packages/plugin-typescript/test/cases/{exp-serve.ssr/exp-serve.ssr.spec.js => loaders-serve.ssr/loaders-serve.ssr.spec.js} (100%) rename packages/plugin-typescript/test/cases/{exp-serve.ssr => loaders-serve.ssr}/package.json (100%) rename packages/plugin-typescript/test/cases/{exp-serve.ssr => loaders-serve.ssr}/src/api/fragment.js (100%) rename packages/plugin-typescript/test/cases/{exp-serve.ssr => loaders-serve.ssr}/src/components/card/card.ts (100%) rename packages/plugin-typescript/test/cases/{exp-serve.ssr => loaders-serve.ssr}/src/components/card/logo.png (100%) rename packages/plugin-typescript/test/cases/{exp-serve.ssr => loaders-serve.ssr}/src/components/card/styles.ts (100%) rename packages/plugin-typescript/test/cases/{exp-serve.ssr => loaders-serve.ssr}/src/pages/index.html (100%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0c06c5089..dce41cebc 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,14 +1,27 @@ +// need this custom parser configuration until ESLint natively supports import attributes +// https://github.com/eslint/eslint/discussions/15305#discussioncomment-2508948 module.exports = { - parser: '@typescript-eslint/parser', + parser: '@babel/eslint-parser', parserOptions: { - ecmaVersion: 2018, - sourceType: 'module' + ecmaVersion: 2022, + sourceType: 'module', + requireConfigFile: false, + ecmaFeatures: { + jsx: true + }, + babelOptions: { + plugins: [ + '@babel/plugin-syntax-import-assertions' + ], + presets: ['@babel/preset-react'] + } }, plugins: [ - '@typescript-eslint', 'no-only-tests' ], - extends: 'plugin:markdown/recommended', + // plugin does not seem to work well with custom parsers? + // https://github.com/eslint/eslint-plugin-markdown/discussions/221 + // extends: 'plugin:markdown/recommended-legacy', env: { browser: true, node: false diff --git a/.github/workflows/ci-exp.yml b/.github/workflows/ci-loaders.yml similarity index 89% rename from .github/workflows/ci-exp.yml rename to .github/workflows/ci-loaders.yml index f4916828d..34c079cbd 100644 --- a/.github/workflows/ci-exp.yml +++ b/.github/workflows/ci-loaders.yml @@ -1,4 +1,4 @@ -name: Continuous Integration (Experimental) +name: Continuous Integration (Loaders) on: [pull_request] @@ -25,4 +25,4 @@ jobs: yarn install --frozen-lockfile && yarn lerna bootstrap - name: Test run: | - yarn test:exp \ No newline at end of file + yarn test:loaders \ No newline at end of file diff --git a/.github/workflows/ci-win-exp.yml b/.github/workflows/ci-win-loaders.yml similarity index 85% rename from .github/workflows/ci-win-exp.yml rename to .github/workflows/ci-win-loaders.yml index ffc7ef0fd..7e69f3873 100644 --- a/.github/workflows/ci-win-exp.yml +++ b/.github/workflows/ci-win-loaders.yml @@ -1,4 +1,4 @@ -name: Continuous Integration Windows (Experimental) +name: Continuous Integration Windows (Loaders) on: [pull_request] @@ -22,4 +22,4 @@ jobs: yarn install --frozen-lockfile --network-timeout 1000000 && yarn lerna bootstrap - name: Test run: | - yarn test:exp:win \ No newline at end of file + yarn test:loaders:win \ No newline at end of file diff --git a/.gitignore b/.gitignore index eaa6b83a2..bd89ae68f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .vscode/ coverage/ node_modules/ +packages/init/test/**/my-app packages/**/test/**/yarn.lock packages/**/test/**/package-lock.json packages/**/test/**/netlify diff --git a/.ls-lint.yml b/.ls-lint.yml index 64ab822b4..ab2ad9cba 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -14,5 +14,6 @@ ls: ignore: - .git - node_modules + - packages/plugin-babel/node_modules - packages/init/node_modules - packages/plugin-typescript/node_modules \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 72c7744b3..23cc58a71 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.12.1 \ No newline at end of file +18.20.0 \ No newline at end of file diff --git a/greenwood.config.js b/greenwood.config.js index 648fdc361..216952137 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -1,9 +1,8 @@ import { greenwoodPluginGraphQL } from '@greenwood/plugin-graphql'; import { greenwoodPluginIncludeHTML } from '@greenwood/plugin-include-html'; -import { greenwoodPluginImportCss } from '@greenwood/plugin-import-css'; -import { greenwoodPluginImportJson } from '@greenwood/plugin-import-json'; import { greenwoodPluginPolyfills } from '@greenwood/plugin-polyfills'; import { greenwoodPluginPostCss } from '@greenwood/plugin-postcss'; +import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; import { greenwoodPluginRendererPuppeteer } from '@greenwood/plugin-renderer-puppeteer'; import rollupPluginAnalyzer from 'rollup-plugin-analyzer'; @@ -18,8 +17,12 @@ export default { lit: true }), greenwoodPluginPostCss(), - greenwoodPluginImportJson(), - greenwoodPluginImportCss(), + greenwoodPluginImportRaw({ + matches: [ + 'eve-button.css', + 'eve-container.css' + ] + }), greenwoodPluginIncludeHTML(), greenwoodPluginRendererPuppeteer(), { diff --git a/package.json b/package.json index fe99893d8..fe45e2b24 100644 --- a/package.json +++ b/package.json @@ -20,22 +20,24 @@ "build": "cross-env __GWD_ROLLUP_MODE__=strict node . build", "serve": "node . serve", "develop": "node . develop", - "test": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 c8 mocha --exclude \"./packages/**/test/cases/exp-*/**\" \"./packages/**/**/*.spec.js\"", - "test:exp": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --experimental-loader $(pwd)/test/test-loader.js ./node_modules/mocha/bin/mocha \"./packages/**/**/*.spec.js\"", - "test:exp:win": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --experimental-loader file:\\\\%cd%\\test\\test-loader.js ./node_modules/mocha/bin/mocha --exclude \"./packages/init/test/cases/**\" \"./packages/**/**/*.spec.js\"", + "test": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 c8 mocha --exclude \"./packages/**/test/cases/loaders-*/**\" \"./packages/**/**/*.spec.js\"", + "test:loaders": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --loader $(pwd)/test/test-loader.js ./node_modules/mocha/bin/mocha \"./packages/**/**/*.spec.js\"", + "test:loaders:win": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --loader file:\\\\%cd%\\test\\test-loader.js ./node_modules/mocha/bin/mocha --exclude \"./packages/init/test/cases/**\" \"./packages/**/**/*.spec.js\"", "test:tdd": "yarn test --watch", - "lint:js": "eslint \"*.{js,md}\" \"./packages/**/**/*.{js,md}\" \"./test/*.js\" \"./www/**/**/*.{js,md}\"", + "lint:js": "eslint \"*.js\" \"./packages/**/**/*.js\" \"./test/*.js\" \"./www/**/**/*.js\"", "lint:ts": "eslint \"./packages/**/**/*.ts\"", "lint:css": "stylelint \"./www/**/*.js\", \"./www/**/*.css\"", - "lint": "ls-lint && yarn lint:js && yarn lint:ts && yarn lint:css" + "lint": "ls-lint && yarn lint:js && yarn lint:css" }, "resolutions": { "lit": "^3.1.0" }, "devDependencies": { + "@babel/core": "^7.24.4", + "@babel/eslint-parser": "^7.24.1", + "@babel/plugin-syntax-import-assertions": "^7.24.1", + "@babel/preset-react": "^7.24.1", "@ls-lint/ls-lint": "^1.10.0", - "@typescript-eslint/eslint-plugin": "^6.7.5", - "@typescript-eslint/parser": "^6.7.5", "babel-eslint": "^10.1.0", "c8": "^7.10.0", "chai": "^4.2.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 23af05e1d..0143d4d63 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -18,7 +18,7 @@ "NodeJS" ], "engines": { - "node": ">=18.12.1" + "node": ">=18.20.0" }, "bin": { "greenwood": "./src/index.js" @@ -38,7 +38,7 @@ "acorn-walk": "^8.0.0", "commander": "^2.20.0", "css-tree": "^2.2.1", - "es-module-shims": "^1.2.0", + "es-module-shims": "^1.8.3", "front-matter": "^4.0.2", "koa": "^2.13.0", "koa-body": "^6.0.1", @@ -52,7 +52,7 @@ "remark-rehype": "^7.0.0", "rollup": "^3.29.4", "unified": "^9.2.0", - "wc-compiler": "~0.12.1" + "wc-compiler": "~0.13.0" }, "devDependencies": { "@babel/runtime": "^7.10.4", diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js index fba4ef73f..e2cc575fe 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.js @@ -26,6 +26,10 @@ async function interceptPage(url, request, plugins, body) { }); 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); } diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index e9103edc9..73ddb24b9 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -9,7 +9,7 @@ import * as walk from 'acorn-walk'; // https://github.com/rollup/rollup/issues/2121 // would be nice to get rid of this function cleanRollupId(id) { - return id.replace('\x00', ''); + return id.replace('\x00', '').replace('?commonjs-proxy', ''); } function greenwoodResourceLoader (compilation) { @@ -35,25 +35,42 @@ function greenwoodResourceLoader (compilation) { } }, async load(id) { - const idUrl = new URL(`file://${cleanRollupId(id)}`); + let idUrl = new URL(`file://${cleanRollupId(id)}`); const { pathname } = idUrl; const extension = pathname.split('.').pop(); + const headers = { + 'Accept': 'text/javascript', + 'Sec-Fetch-Dest': 'empty' + }; // filter first for any bare specifiers - if (await checkResourceExists(idUrl) && extension !== '' && extension !== 'js') { - const url = new URL(`${idUrl.href}?type=${extension}`); - const request = new Request(url.href); + if (await checkResourceExists(idUrl) && extension !== 'js') { + for (const plugin of resourcePlugins) { + if (plugin.shouldResolve && await plugin.shouldResolve(idUrl)) { + idUrl = new URL((await plugin.resolve(idUrl)).url); + } + } + + const request = new Request(idUrl, { + headers + }); let response = new Response(''); for (const plugin of resourcePlugins) { - if (plugin.shouldServe && await plugin.shouldServe(url, request)) { - response = await plugin.serve(url, request); + if (plugin.shouldServe && await plugin.shouldServe(idUrl, request)) { + response = await plugin.serve(idUrl, request); } } for (const plugin of resourcePlugins) { - if (plugin.shouldIntercept && await plugin.shouldIntercept(url, request, response.clone())) { - response = await plugin.intercept(url, request, response.clone()); + if (plugin.shouldPreIntercept && await plugin.shouldPreIntercept(idUrl, request, response.clone())) { + response = await plugin.preIntercept(idUrl, request, response.clone()); + } + } + + for (const plugin of resourcePlugins) { + if (plugin.shouldIntercept && await plugin.shouldIntercept(idUrl, request, response.clone())) { + response = await plugin.intercept(idUrl, request, response.clone()); } } @@ -161,26 +178,42 @@ function greenwoodImportMetaUrl(compilation) { }); const idAssetName = path.basename(id); const normalizedId = id.replace(/\\\\/g, '/').replace(/\\/g, '/'); // windows shenanigans... - const idUrl = new URL(`file://${cleanRollupId(id)}`); - const { pathname } = idUrl; - const extension = pathname.split('.').pop(); - const urlWithType = new URL(`${idUrl.href}?type=${extension}`); - const request = new Request(urlWithType.href); + let idUrl = new URL(`file://${cleanRollupId(id)}`); + const headers = { + 'Accept': 'text/javascript', + 'Sec-Fetch-Dest': 'empty' + }; + const request = new Request(idUrl, { + headers + }); let canTransform = false; let response = new Response(code); // handle any custom imports or pre-processing needed before passing to Rollup this.parse - if (await checkResourceExists(idUrl) && extension !== '' && extension !== 'json') { + if (await checkResourceExists(idUrl)) { + for (const plugin of resourcePlugins) { + if (plugin.shouldResolve && await plugin.shouldResolve(idUrl)) { + idUrl = new URL((await plugin.resolve(idUrl)).url); + } + } + + for (const plugin of resourcePlugins) { + if (plugin.shouldServe && await plugin.shouldServe(idUrl, request)) { + response = await plugin.serve(idUrl, request); + canTransform = true; + } + } + for (const plugin of resourcePlugins) { - if (plugin.shouldServe && await plugin.shouldServe(urlWithType, request)) { - response = await plugin.serve(urlWithType, request); + if (plugin.shouldPreIntercept && await plugin.shouldPreIntercept(idUrl, request, response)) { + response = await plugin.preIntercept(idUrl, request, response); canTransform = true; } } for (const plugin of resourcePlugins) { - if (plugin.shouldIntercept && await plugin.shouldIntercept(urlWithType, request, response.clone())) { - response = await plugin.intercept(urlWithType, request, response.clone()); + if (plugin.shouldIntercept && await plugin.shouldIntercept(idUrl, request, response.clone())) { + response = await plugin.intercept(idUrl, request, response.clone()); canTransform = true; } } @@ -201,11 +234,9 @@ function greenwoodImportMetaUrl(compilation) { const absoluteScriptDir = path.dirname(id); const relativeAssetPath = getMetaImportPath(node); const absoluteAssetPath = path.resolve(absoluteScriptDir, relativeAssetPath); - const assetName = path.basename(absoluteAssetPath); - const assetExtension = assetName.split('.').pop(); assetUrls.push({ - url: new URL(`file://${absoluteAssetPath}?type=${assetExtension}`), + url: new URL(`file://${absoluteAssetPath}`), relativeAssetPath }); } diff --git a/packages/cli/src/lib/resource-utils.js b/packages/cli/src/lib/resource-utils.js index 1032afc5e..a08cd9c57 100644 --- a/packages/cli/src/lib/resource-utils.js +++ b/packages/cli/src/lib/resource-utils.js @@ -61,15 +61,18 @@ function mergeResponse(destination, source) { // https://github.com/rollup/rollup/issues/3779 function normalizePathnameForWindows(url) { const windowsDriveRegex = /\/[a-zA-Z]{1}:\//; - const { pathname = '' } = url; + const { pathname = '', searchParams } = url; + const params = searchParams.size > 0 + ? `?${searchParams.toString()}` + : ''; if (windowsDriveRegex.test(pathname)) { const driveMatch = pathname.match(windowsDriveRegex)[0]; - return pathname.replace(driveMatch, driveMatch.replace('/', '')); + return `${pathname.replace(driveMatch, driveMatch.replace('/', ''))}${params}`; } - return pathname; + return `${pathname}${params}`; } async function checkResourceExists(url) { @@ -108,7 +111,7 @@ async function resolveForRelativeUrl(url, rootUrl) { return reducedUrl; } -// TODO does this make more sense in bundle lifecycle? +// 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 diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index e3a037a7e..82686250c 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -13,6 +13,10 @@ async function interceptPage(url, request, plugins, body) { }); 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); } @@ -140,11 +144,27 @@ async function bundleStyleResources(compilation, resourcePlugins) { } else { const url = resource.sourcePathURL; const contentType = 'text/css'; - const headers = new Headers({ 'Content-Type': contentType }); + const headers = new Headers({ 'Content-Type': contentType, 'Accept': contentType }); const request = new Request(url, { headers }); const initResponse = new Response(contents, { headers }); let response = await resourcePlugins.reduce(async (responsePromise, plugin) => { + const intermediateResponse = await responsePromise; + const shouldPreIntercept = plugin.shouldPreIntercept && await plugin.shouldPreIntercept(url, request, intermediateResponse.clone()); + + if (shouldPreIntercept) { + const currentResponse = await plugin.preIntercept(url, request, intermediateResponse.clone()); + const mergedResponse = mergeResponse(intermediateResponse.clone(), currentResponse.clone()); + + if (mergedResponse.headers.get('Content-Type').indexOf(contentType) >= 0) { + return Promise.resolve(mergedResponse.clone()); + } + } + + return Promise.resolve(responsePromise); + }, Promise.resolve(initResponse)); + + response = await resourcePlugins.reduce(async (responsePromise, plugin) => { const intermediateResponse = await responsePromise; const shouldIntercept = plugin.shouldIntercept && await plugin.shouldIntercept(url, request, intermediateResponse.clone()); @@ -158,7 +178,7 @@ async function bundleStyleResources(compilation, resourcePlugins) { } return Promise.resolve(responsePromise); - }, Promise.resolve(initResponse)); + }, Promise.resolve(response.clone())); response = await resourcePlugins.reduce(async (responsePromise, plugin) => { const intermediateResponse = await responsePromise; @@ -295,6 +315,7 @@ const bundleCompilation = async (compilation) => { return plugin.provider(compilation); }).filter((provider) => { return provider.shouldIntercept && provider.intercept + || provider.shouldPreIntercept && provider.preIntercept || provider.shouldOptimize && provider.optimize; }); diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 6eb1a3438..804cfd4a1 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -32,6 +32,10 @@ async function interceptPage(url, request, plugins, body) { }); 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); } diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 6ae901876..6201ea80f 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -88,6 +88,42 @@ async function getDevServer(compilation) { await next(); }); + // allow pre-processing of userland plugins _before_ Greenwood "standardizes" it + app.use(async (ctx, next) => { + try { + const url = new URL(ctx.url); + const { header, status, message } = ctx.response; + const request = transformKoaRequestIntoStandardRequest(url, ctx.request); + const initResponse = new Response(status === 204 ? null : ctx.body, { + statusText: message, + status, + headers: new Headers(header) + }); + const response = await resourcePlugins.reduce(async (responsePromise, plugin) => { + const intermediateResponse = await responsePromise; + if (plugin.shouldPreIntercept && await plugin.shouldPreIntercept(url, request, intermediateResponse.clone())) { + const current = await plugin.preIntercept(url, request, await intermediateResponse.clone()); + const merged = mergeResponse(intermediateResponse.clone(), current); + + return Promise.resolve(merged); + } else { + return Promise.resolve(await responsePromise); + } + }, Promise.resolve(initResponse.clone())); + + ctx.body = response.body ? Readable.from(response.body) : ''; + ctx.message = response.statusText; + response.headers.forEach((value, key) => { + ctx.set(key, value); + }); + } catch (e) { + ctx.status = 500; + console.error(e); + } + + await next(); + }); + // allow intercepting of responses for URLs app.use(async (ctx, next) => { try { diff --git a/packages/cli/src/loader.js b/packages/cli/src/loader.js index 238fefcd0..5d347bc20 100644 --- a/packages/cli/src/loader.js +++ b/packages/cli/src/loader.js @@ -1,19 +1,23 @@ -import fs from 'fs/promises'; import { readAndMergeConfig as initConfig } from './lifecycles/config.js'; const config = await initConfig(); -const resourcePlugins = config.plugins.filter(plugin => plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin).map(plugin => plugin.provider({ +const resourcePlugins = config.plugins.filter(plugin => plugin.type === 'resource').map(plugin => plugin.provider({ context: { projectDirectory: new URL(`file://${process.cwd()}`) - } + }, + config: { + devServer: {} + }, + graph: [] })); -async function getCustomLoaderResponse(url, body = '', checkOnly = false) { - const headers = new Headers({ - 'Content-Type': 'text/javascript' - }); - const request = new Request(url.href, { headers }); - const initResponse = new Response(body, { headers }); +async function getCustomLoaderResponse(url, checkOnly = false) { + const headers = { + 'Accept': 'text/javascript', + 'Sec-Fetch-Dest': 'empty' + }; + const request = new Request(url, { headers }); + const initResponse = new Response(''); let response = initResponse.clone(); let shouldHandle = false; @@ -28,6 +32,14 @@ async function getCustomLoaderResponse(url, body = '', checkOnly = false) { } for (const plugin of resourcePlugins) { + if (plugin.shouldPreIntercept && await plugin.shouldPreIntercept(url, request, response.clone())) { + shouldHandle = true; + + if (!checkOnly) { + response = await plugin.preIntercept(url, request, response.clone()); + } + } + if (plugin.shouldIntercept && await plugin.shouldIntercept(url, request, response.clone())) { shouldHandle = true; @@ -53,7 +65,7 @@ export async function resolve(specifier, context, defaultResolve) { : undefined; if (url) { - const { shouldHandle } = await getCustomLoaderResponse(url, null, true); + const { shouldHandle } = await getCustomLoaderResponse(url, true); if (shouldHandle) { return { @@ -69,19 +81,16 @@ export async function resolve(specifier, context, defaultResolve) { // https://nodejs.org/docs/latest-v18.x/api/esm.html#loadurl-context-nextload export async function load(source, context, defaultLoad) { const extension = source.split('.').pop(); - const url = new URL(`${source}?type=${extension}`); - const { shouldHandle } = await getCustomLoaderResponse(url, null, true); + const url = new URL(source); + const { shouldHandle } = await getCustomLoaderResponse(url, true); - if (shouldHandle) { - const contents = await fs.readFile(url, 'utf-8'); - const { response } = await getCustomLoaderResponse(url, contents); - const body = await response.text(); + if (shouldHandle && extension !== 'js') { + const { response } = await getCustomLoaderResponse(url); + const contents = await response.text(); - // TODO better way to handle remove export default? leverage import assertions instead - // https://github.com/ProjectEvergreen/greenwood/issues/923 return { - format: extension === 'json' ? 'json' : 'module', - source: extension === 'json' ? JSON.stringify(JSON.parse(contents.replace('export default ', ''))) : body, + format: 'module', + source: contents, shortCircuit: true }; } diff --git a/packages/cli/src/plugins/resource/plugin-node-modules.js b/packages/cli/src/plugins/resource/plugin-node-modules.js index 475efbf18..ff05c4446 100644 --- a/packages/cli/src/plugins/resource/plugin-node-modules.js +++ b/packages/cli/src/plugins/resource/plugin-node-modules.js @@ -29,7 +29,7 @@ class NodeModulesResource extends ResourceInterface { // https://github.com/ProjectEvergreen/greenwood/issues/953v async resolve(url) { const { projectDirectory } = this.compilation.context; - const { pathname } = url; + const { pathname, searchParams } = url; const packageName = getPackageNameFromUrl(pathname); const absoluteNodeModulesLocation = await getNodeModulesLocationForPackage(packageName); const packagePathPieces = pathname.split('node_modules/')[1].split('/'); // double split to handle node_modules within nested paths @@ -37,8 +37,11 @@ class NodeModulesResource extends ResourceInterface { const absoluteNodeModulesPathname = absoluteNodeModulesLocation ? `${absoluteNodeModulesLocation}${packagePathPieces.join('/').replace(packageName, '')}` : (await resolveForRelativeUrl(url, projectDirectory)).pathname; + const params = searchParams.size > 0 + ? `?${searchParams.toString()}` + : ''; - return new Request(`file://${absoluteNodeModulesPathname}`); + return new Request(`file://${absoluteNodeModulesPathname}${params}`); } async shouldServe(url) { diff --git a/packages/cli/src/plugins/resource/plugin-standard-css.js b/packages/cli/src/plugins/resource/plugin-standard-css.js index d52c723e2..f9cfbe664 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-css.js +++ b/packages/cli/src/plugins/resource/plugin-standard-css.js @@ -227,11 +227,30 @@ class StandardCssResource extends ResourceInterface { }); } + async shouldIntercept(url, request) { + const { pathname, searchParams } = url; + const ext = pathname.split('.').pop(); + + return url.protocol === 'file:' && ext === this.extensions[0] && request.headers.get('Accept')?.indexOf('text/javascript') >= 0 && !searchParams.has('type'); + } + + async intercept(url, request, response) { + const contents = (await response.text()).replace(/\r?\n|\r/g, ' ').replace(/\\/g, '\\\\'); + const body = `const sheet = new CSSStyleSheet();sheet.replaceSync(\`${contents}\`);export default sheet;`; + + return new Response(body, { + headers: { + 'Content-Type': 'text/javascript' + } + }); + } + async shouldOptimize(url, response) { - const { protocol, pathname } = url; + const { protocol, pathname, searchParams } = url; const isValidCss = pathname.split('.').pop() === this.extensions[0] && protocol === 'file:' - && response.headers.get('Content-Type').indexOf(this.contentType) >= 0; + && response.headers.get('Content-Type').indexOf(this.contentType) >= 0 + && searchParams.get('type') !== 'css'; return this.compilation.config.optimization !== 'none' && isValidCss; } diff --git a/packages/cli/src/plugins/resource/plugin-standard-json.js b/packages/cli/src/plugins/resource/plugin-standard-json.js index 726c9b5c1..07d9883aa 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-json.js +++ b/packages/cli/src/plugins/resource/plugin-standard-json.js @@ -40,6 +40,24 @@ class StandardJsonResource extends ResourceInterface { }) }); } + + async shouldIntercept(url, request) { + const { protocol, pathname, searchParams } = url; + const ext = pathname.split('.').pop(); + + return protocol === 'file:' && request.headers.get('Accept')?.indexOf('text/javascript') >= 0 && ext === this.extensions[0] && !searchParams.has('type'); + } + + async intercept(url, request, response) { + const json = await response.json(); + const body = `export default ${JSON.stringify(json)}`; + + return new Response(body, { + headers: { + 'Content-Type': 'text/javascript' + } + }); + } } const pluginGreenwoodStandardJson = [{ diff --git a/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js b/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js index bff573f54..2227fbad2 100644 --- a/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js +++ b/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js @@ -1,20 +1,15 @@ import fs from 'fs/promises'; -import os from 'os'; -import { spawnSync } from 'child_process'; const packageJson = JSON.parse(await fs.readFile(new URL('./package.json', import.meta.url), 'utf-8')); -const myThemePackPlugin = () => [{ +const myThemePackPlugin = (options = {}) => [{ type: 'context', name: 'my-theme-pack:context', provider: () => { const { name } = packageJson; - const command = os.platform() === 'win32' ? 'npm.cmd' : 'npm'; - const ls = spawnSync(command, ['ls', name]); - const isInstalled = ls.stdout.toString().indexOf('(empty)') < 0; - const templateLocation = isInstalled - ? new URL(`./node_modules/${name}/dist/layouts/`, import.meta.url) - : new URL('./fixtures/layouts/', import.meta.url); + const templateLocation = options.__isDevelopment // eslint-disable-line no-underscore-dangle + ? new URL('./fixtures/layouts/', import.meta.url) + : new URL(`./node_modules/${name}/dist/layouts/`, import.meta.url); return { templates: [ diff --git a/packages/cli/test/cases/develop.plugins.context/greenwood.config.js b/packages/cli/test/cases/develop.plugins.context/greenwood.config.js index 74c311257..60eaf3c94 100644 --- a/packages/cli/test/cases/develop.plugins.context/greenwood.config.js +++ b/packages/cli/test/cases/develop.plugins.context/greenwood.config.js @@ -24,7 +24,9 @@ class MyThemePackDevelopmentResource extends ResourceInterface { export default { plugins: [ - ...myThemePackPlugin(), + ...myThemePackPlugin({ + __isDevelopment: true + }), { type: 'resource', name: 'my-theme-pack:resource', diff --git a/packages/cli/test/cases/loaders-build.import-attributes/loaders-build.import-attributes.spec.js b/packages/cli/test/cases/loaders-build.import-attributes/loaders-build.import-attributes.spec.js new file mode 100644 index 000000000..7d474741c --- /dev/null +++ b/packages/cli/test/cases/loaders-build.import-attributes/loaders-build.import-attributes.spec.js @@ -0,0 +1,77 @@ +/* + * Use Case + * Run Greenwood serve command with no config for using import attributes with a basic static bundles. + * + * User Result + * Should start the development server and render a bare bones Greenwood build. + * + * User Command + * greenwood serve + * + * User Config + * {} + * + * User Workspace + * src/ + * components/ + * card/ + * card.css + * card.js + * card.json + * pages/ + * index.html + * + */ +import chai from 'chai'; +import fs from 'fs'; +import glob from 'glob-promise'; +import path from 'path'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With: ', function() { + const LABEL = 'Import Attributes used in static pages'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://localhost:8080'; + let runner; + + before(function() { + this.context = { + hostname + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + + before(async function() { + runner.setup(outputPath); + runner.runCommand(cliPath, 'build'); + }); + + describe('Importing CSS w/ Constructable Stylesheets', function() { + let scripts; + + before(async function() { + scripts = await glob.promise(path.join(outputPath, 'public/card.*.js')); + }); + + it('should have the expected output from importing hero.css as a Constructable Stylesheet', function() { + const scriptContents = fs.readFileSync(scripts[0], 'utf-8'); + + expect(scriptContents).to.contain('const e=new CSSStyleSheet;e.replaceSync(":host { color: red; }");'); + }); + }); + }); + + after(function() { + runner.stopCommand(); + runner.teardown([ + path.join(outputPath, '.greenwood'), + path.join(outputPath, 'node_modules') + ]); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.css b/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.css new file mode 100644 index 000000000..2ace1c18d --- /dev/null +++ b/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.css @@ -0,0 +1,3 @@ +:host { + color: red; +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.js b/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.js new file mode 100644 index 000000000..49a154086 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.js @@ -0,0 +1,27 @@ +import sheet from './card.css' with { type: 'css' }; +import data from './card.json' with { type: 'json' }; + +export default class Card extends HTMLElement { + + connectedCallback() { + if (!this.shadowRoot) { + const name = this.getAttribute('name') || 'World'; + const template = document.createElement('template'); + + template.innerHTML = ` +
+ logo +

Hello, ${name}!

+
+
+ `; + + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + this.shadowRoot.adoptedStyleSheets = [sheet]; + } +} + +customElements.define('app-card', Card); \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.json b/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.json new file mode 100644 index 000000000..cf759ab71 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.import-attributes/src/components/card/card.json @@ -0,0 +1,5 @@ +{ + "image": { + "url": "/path/to/image.webp" + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.import-attributes/src/pages/index.html b/packages/cli/test/cases/loaders-build.import-attributes/src/pages/index.html new file mode 100644 index 000000000..ed61c4594 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.import-attributes/src/pages/index.html @@ -0,0 +1,11 @@ + + + + + + + +

The home page

+ + + \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.prerender-import-attributes/greenwood.config.js b/packages/cli/test/cases/loaders-build.prerender-import-attributes/greenwood.config.js new file mode 100644 index 000000000..8dc4be464 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.prerender-import-attributes/greenwood.config.js @@ -0,0 +1,3 @@ +export default { + prerender: true +}; \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.prerender-import-attributes/loaders-build.prerender-import-attributes.spec.js b/packages/cli/test/cases/loaders-build.prerender-import-attributes/loaders-build.prerender-import-attributes.spec.js new file mode 100644 index 000000000..7b765e769 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.prerender-import-attributes/loaders-build.prerender-import-attributes.spec.js @@ -0,0 +1,97 @@ +/* + * Use Case + * Run Greenwood with prerendering of CSS and JSON being referenced using import attributes. + * + * User Result + * Should generate a static Greenwood build with CSS properly prerendered. + * + * User Command + * greenwood build + * + * User Config + * import { greenwoodPluginImportCss } from '@greenwood/plugin-import-css'; + * + * { + * prerender: true, + * } + * + * User Workspace + * src/ + * components/ + * hero/ + * hero.css + * hero.js + * hero.json +* index.html + */ +import chai from 'chai'; +import fs from 'fs'; +import glob from 'glob-promise'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With: ', function() { + const LABEL = 'ESM Import Attribute for CSS and JSON with prerendering'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + before(function() { + runner.setup(outputPath, getSetupFiles(outputPath)); + runner.runCommand(cliPath, 'build'); + }); + + runSmokeTest(['public'], LABEL); + + describe('Importing CSS w/ Constructable Stylesheets', function() { + let scripts; + + before(async function() { + scripts = await glob.promise(path.join(this.context.publicDir, '*.js')); + }); + + // TODO is this actually the output we want here? + // https://github.com/ProjectEvergreen/greenwood/discussions/1216 + it('should have the expected output from importing hero.css as a Constructable Stylesheet', function() { + const scriptContents = fs.readFileSync(scripts[0], 'utf-8'); + + expect(scriptContents).to.contain('const t=new CSSStyleSheet;t.replaceSync(":host { text-align: center'); + }); + }); + + describe('Importing JSON', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should have the expected inline content from the JSON file', function() { + const hero = new JSDOM(dom.window.document.querySelector('app-hero template[shadowrootmode="open"]').innerHTML); + const heading = hero.window.document.querySelectorAll('div.hero h2'); + + expect(heading.length).to.equal(1); + expect(heading[0].textContent).to.be.equal('Welcome to my website'); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.prerender-import-attributes/package.json b/packages/cli/test/cases/loaders-build.prerender-import-attributes/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.prerender-import-attributes/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.css b/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.css new file mode 100644 index 000000000..86fb17fc3 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.css @@ -0,0 +1,18 @@ +:host { + text-align: center; + margin-bottom: 40px; +} + +:host h2 { + font-size: 3em; +} + +:host button { + display: inline-block; + background-color: var(--color-primary); + color: var(--color-white); + font-size: 1.5em; + padding: 14px; + border-radius: 10px; + cursor: pointer; +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.js b/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.js new file mode 100644 index 000000000..9ef6625cb --- /dev/null +++ b/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.js @@ -0,0 +1,40 @@ +import sheet from './hero.css' with { type: 'css' }; +import data from './hero.json' with { type: 'json' }; + +export default class HeroBanner extends HTMLElement { + clickButton(el) { + console.log('clicked button =>', el.textContent); + } + + connectedCallback() { + if (!this.shadowRoot) { + const template = document.createElement('template'); + + template.innerHTML = ` +
+

${data.message}

+ + + + + + + + +
+ `; + + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + this.shadowRoot.adoptedStyleSheets = [sheet]; + // TODO upstream to WCC? + // this.shadowRoot.querySelectorAll('button') + // .forEach(button => { + // button.addEventListener('click', () => this.clickButton(button)) + // }); + } +} + +customElements.define('app-hero', HeroBanner); \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.json b/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.json new file mode 100644 index 000000000..662f223f5 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/components/hero/hero.json @@ -0,0 +1,3 @@ +{ + "message": "Welcome to my website" +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/index.html b/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/index.html new file mode 100644 index 000000000..f67c1b687 --- /dev/null +++ b/packages/cli/test/cases/loaders-build.prerender-import-attributes/src/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-develop.ssr-import-attributes/loaders-develop.ssr-import-attributes.spec.js b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/loaders-develop.ssr-import-attributes.spec.js new file mode 100644 index 000000000..4e8ba8745 --- /dev/null +++ b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/loaders-develop.ssr-import-attributes.spec.js @@ -0,0 +1,186 @@ +/* + * Use Case + * Run Greenwood develop command with no config. + * + * User Result + * Should start the development server and render a bare bones Greenwood build. + * + * User Command + * greenwood develop + * + * User Config + * {} + * + * User Workspace + * src/ + * api/ + * fragment.js + * components/ + * card/ + * card.css + * card.js + * card.json + * pages/ + * greeting.js + * + */ +import chai from 'chai'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Develop Greenwood With: ', function() { + const LABEL = 'Import Attributes used in API Routes and SSR Pages'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://127.0.0.1:1984'; + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + + before(async function() { + runner.setup(outputPath); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 5000); + + runner.runCommand(cliPath, 'develop', { async: true }); + }); + }); + + describe('CSS file is returned as CSS (text/css)', function() { + let response = {}; + let body; + + before(async function() { + response = await fetch(`${hostname}/components/card/card.css`); + body = await response.clone().text(); + }); + + it('should return a 200 status', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.equal('text/css'); + done(); + }); + + it('should return the correct response body', function(done) { + expect(body).to.equal(':host {\n color: red;\n}'); + done(); + }); + }); + + describe('JSON file is returned as JSON (application/json)', function() { + let response = {}; + let data; + + before(async function() { + response = await fetch(`${hostname}/components/card/card.json`); + data = await response.clone().json(); + }); + + it('should return a 200 status', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.equal('application/json'); + done(); + }); + + it('should return the correct response body data', function(done) { + expect(data.image.url).to.equal('/path/to/image.webp'); + done(); + }); + }); + + describe('API Route specific behaviors for an HTML ("fragment") API', function() { + const name = 'Greenwood'; + let response = {}; + let body; + + before(async function() { + response = await fetch(`${hostname}/api/fragment?name=${name}`); + body = await response.clone().text(); + }); + + it('should return a 200 status', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.equal('text/html'); + done(); + }); + + it('should return the correct response body', function(done) { + const dom = new JSDOM(body); + const card = new JSDOM(dom.window.document.querySelectorAll('app-card template[shadowrootmode="open"]')[0].innerHTML); + const heading = card.window.document.querySelector('h2'); + const image = card.window.document.querySelector('img'); + + expect(heading.textContent).to.equal(`Hello, ${name}!`); + expect(image.getAttribute('href')).to.equal('/path/to/image.webp'); + + done(); + }); + }); + + describe('SSR route specific behaviors when using a custom element as the page', function() { + let response = {}; + let body; + + before(async function() { + response = await fetch(`${hostname}/greeting/`); + body = await response.clone().text(); + }); + + it('should return a 200 status', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.equal('text/html'); + done(); + }); + + it('should return the correct response body', function(done) { + const dom = new JSDOM(body); + const card = new JSDOM(dom.window.document.querySelectorAll('app-card template[shadowrootmode="open"]')[0].innerHTML); + const heading = card.window.document.querySelector('h2'); + const image = card.window.document.querySelector('img'); + + expect(heading.textContent).to.equal('Hello, World!'); + expect(image.getAttribute('href')).to.equal('/path/to/image.webp'); + + done(); + }); + }); + }); + + after(function() { + runner.stopCommand(); + runner.teardown([ + path.join(outputPath, '.greenwood'), + path.join(outputPath, 'node_modules') + ]); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/api/fragment.js b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/api/fragment.js new file mode 100644 index 000000000..a37a035db --- /dev/null +++ b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/api/fragment.js @@ -0,0 +1,17 @@ +import { renderFromHTML } from 'wc-compiler'; + +export async function handler(request) { + const params = new URLSearchParams(request.url.slice(request.url.indexOf('?'))); + const name = params.has('name') ? params.get('name') : 'World'; + const { html } = await renderFromHTML(` + + `, [ + new URL('../components/card/card.js', import.meta.url) + ]); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.css b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.css new file mode 100644 index 000000000..2ace1c18d --- /dev/null +++ b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.css @@ -0,0 +1,3 @@ +:host { + color: red; +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.js b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.js new file mode 100644 index 000000000..49a154086 --- /dev/null +++ b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.js @@ -0,0 +1,27 @@ +import sheet from './card.css' with { type: 'css' }; +import data from './card.json' with { type: 'json' }; + +export default class Card extends HTMLElement { + + connectedCallback() { + if (!this.shadowRoot) { + const name = this.getAttribute('name') || 'World'; + const template = document.createElement('template'); + + template.innerHTML = ` +
+ logo +

Hello, ${name}!

+
+
+ `; + + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + this.shadowRoot.adoptedStyleSheets = [sheet]; + } +} + +customElements.define('app-card', Card); \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.json b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.json new file mode 100644 index 000000000..cf759ab71 --- /dev/null +++ b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/components/card/card.json @@ -0,0 +1,5 @@ +{ + "image": { + "url": "/path/to/image.webp" + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/pages/greeting.js b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/pages/greeting.js new file mode 100644 index 000000000..4e69e3014 --- /dev/null +++ b/packages/cli/test/cases/loaders-develop.ssr-import-attributes/src/pages/greeting.js @@ -0,0 +1,10 @@ +import '../components/card/card.js'; + +export default class GreetingPage extends HTMLElement { + + async connectedCallback() { + this.innerHTML = ` + + `; + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/loaders-serve.default.ssr-import-attributes.spec.js b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/loaders-serve.default.ssr-import-attributes.spec.js new file mode 100644 index 000000000..816e2ee91 --- /dev/null +++ b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/loaders-serve.default.ssr-import-attributes.spec.js @@ -0,0 +1,167 @@ +/* + * Use Case + * Run Greenwood serve command with no config for using import attributes with API Routes and SSR pages.. + * + * User Result + * Should start the development server and render a bare bones Greenwood build. + * + * User Command + * greenwood serve + * + * User Config + * {} + * + * User Workspace + * src/ + * api/ + * fragment.js + * components/ + * card/ + * card.css + * card.js + * card.json + * pages/ + * greeting.js + * + */ +import chai from 'chai'; +import fs from 'fs'; +import glob from 'glob-promise'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Serve Greenwood With: ', function() { + const LABEL = 'Import Attributes used in API Routes and SSR Pages'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://localhost:8080'; + let runner; + + before(function() { + this.context = { + hostname + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + + before(async function() { + runner.setup(outputPath); + runner.runCommand(cliPath, 'build'); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + + runner.runCommand(cliPath, 'serve', { async: true }); + }); + }); + + describe('API Route specific behaviors for an HTML ("fragment") API', function() { + const name = 'Greenwood'; + let response = {}; + let body; + let scripts; + + before(async function() { + response = await fetch(`${hostname}/api/fragment?name=${name}`); + body = await response.clone().text(); + scripts = await glob.promise(path.join(outputPath, 'public/api/card.*.js')); + }); + + it('should return a 200 status', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.equal('text/html'); + done(); + }); + + it('should return the correct response body', function(done) { + const dom = new JSDOM(body); + const card = new JSDOM(dom.window.document.querySelectorAll('app-card template[shadowrootmode="open"]')[0].innerHTML); + const heading = card.window.document.querySelector('h2'); + const image = card.window.document.querySelector('img'); + + expect(heading.textContent).to.equal(`Hello, ${name}!`); + expect(image.getAttribute('href')).to.equal('/path/to/image.webp'); + + done(); + }); + + it('should have the expected output from importing hero.css as a Constructable Stylesheet', function() { + const scriptContents = fs.readFileSync(scripts[0], 'utf-8'); + + expect(scriptContents).to.contain('const sheet = new CSSStyleSheet();sheet.replaceSync(`:host { color: red; }`);'); + }); + + it('should have the expected output from importing hero.json', function() { + const scriptContents = fs.readFileSync(scripts[0], 'utf-8'); + + expect(scriptContents).to.contain('var data = {"image":{"url":"/path/to/image.webp"}};'); + }); + }); + + describe('SSR route specific behaviors when using a custom element as the page', function() { + let response = {}; + let body; + let scripts; + + before(async function() { + response = await fetch(`${hostname}/greeting/`); + body = await response.clone().text(); + scripts = await glob.promise(path.join(outputPath, 'public/greeting.route.chunk.*.js')); + }); + + it('should return a 200 status', function(done) { + expect(response.status).to.equal(200); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers.get('content-type')).to.equal('text/html'); + done(); + }); + + it('should return the correct response body', function(done) { + const dom = new JSDOM(body); + const card = new JSDOM(dom.window.document.querySelectorAll('app-card template[shadowrootmode="open"]')[0].innerHTML); + const heading = card.window.document.querySelector('h2'); + const image = card.window.document.querySelector('img'); + + expect(heading.textContent).to.equal('Hello, World!'); + expect(image.getAttribute('href')).to.equal('/path/to/image.webp'); + + done(); + }); + + it('should have the expected output from importing hero.css as a Constructable Stylesheet', function() { + const scriptContents = fs.readFileSync(scripts[0], 'utf-8'); + + expect(scriptContents).to.contain('const sheet = new CSSStyleSheet();sheet.replaceSync(`:host { color: red; }`);'); + }); + + it('should have the expected output from importing hero.json', function() { + const scriptContents = fs.readFileSync(scripts[0], 'utf-8'); + + expect(scriptContents).to.contain('var data = {"image":{"url":"/path/to/image.webp"}};'); + }); + }); + }); + + after(function() { + runner.stopCommand(); + runner.teardown([ + path.join(outputPath, '.greenwood'), + path.join(outputPath, 'node_modules') + ]); + }); +}); \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/api/fragment.js b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/api/fragment.js new file mode 100644 index 000000000..a37a035db --- /dev/null +++ b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/api/fragment.js @@ -0,0 +1,17 @@ +import { renderFromHTML } from 'wc-compiler'; + +export async function handler(request) { + const params = new URLSearchParams(request.url.slice(request.url.indexOf('?'))); + const name = params.has('name') ? params.get('name') : 'World'; + const { html } = await renderFromHTML(` + + `, [ + new URL('../components/card/card.js', import.meta.url) + ]); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.css b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.css new file mode 100644 index 000000000..2ace1c18d --- /dev/null +++ b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.css @@ -0,0 +1,3 @@ +:host { + color: red; +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.js b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.js new file mode 100644 index 000000000..49a154086 --- /dev/null +++ b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.js @@ -0,0 +1,27 @@ +import sheet from './card.css' with { type: 'css' }; +import data from './card.json' with { type: 'json' }; + +export default class Card extends HTMLElement { + + connectedCallback() { + if (!this.shadowRoot) { + const name = this.getAttribute('name') || 'World'; + const template = document.createElement('template'); + + template.innerHTML = ` +
+ logo +

Hello, ${name}!

+
+
+ `; + + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + this.shadowRoot.adoptedStyleSheets = [sheet]; + } +} + +customElements.define('app-card', Card); \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.json b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.json new file mode 100644 index 000000000..cf759ab71 --- /dev/null +++ b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/components/card/card.json @@ -0,0 +1,5 @@ +{ + "image": { + "url": "/path/to/image.webp" + } +} \ No newline at end of file diff --git a/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/pages/greeting.js b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/pages/greeting.js new file mode 100644 index 000000000..4e69e3014 --- /dev/null +++ b/packages/cli/test/cases/loaders-serve.default.ssr-import-attributes/src/pages/greeting.js @@ -0,0 +1,10 @@ +import '../components/card/card.js'; + +export default class GreetingPage extends HTMLElement { + + async connectedCallback() { + this.innerHTML = ` + + `; + } +} \ No newline at end of file diff --git a/packages/plugin-babel/src/index.js b/packages/plugin-babel/src/index.js index d4881e915..a3a6b3178 100644 --- a/packages/plugin-babel/src/index.js +++ b/packages/plugin-babel/src/index.js @@ -37,11 +37,11 @@ class BabelResource extends ResourceInterface { this.contentType = ['text/javascript']; } - async shouldIntercept(url) { + async shouldPreIntercept(url) { return url.pathname.split('.').pop() === this.extensions[0] && !url.pathname.startsWith('/node_modules/'); } - async intercept(url, request, response) { + async preIntercept(url, request, response) { const config = await getConfig(this.compilation, this.options.extendConfig); const body = await response.text(); const result = await babel.transform(body, config); diff --git a/packages/plugin-graphql/README.md b/packages/plugin-graphql/README.md index 21f37bc41..243446981 100644 --- a/packages/plugin-graphql/README.md +++ b/packages/plugin-graphql/README.md @@ -45,7 +45,7 @@ This will then allow you to use GraphQL to query your content from your client s ```js import client from '@greenwood/plugin-graphql/src/core/client.js'; -import MenuQuery from '@greenwood/plugin-graphql/src/queries/menu.gql'; +import MenuQuery from '@greenwood/plugin-graphql/src/queries/menu.gql' with { type: 'gql' }; class HeaderComponent extends HTMLElement { constructor() { @@ -167,7 +167,7 @@ query($name: String!) { And then you can use it in your code as such: ```js import client from '@greenwood/plugin-graphql/src/core/client.js'; -import GalleryQuery from '../relative/path/to/data/queries/gallery.gql'; +import GalleryQuery from '../relative/path/to/data/queries/gallery.gql' with { type: 'gql' }; client.query({ query: GalleryQuery, diff --git a/packages/plugin-graphql/test/cases/exp-prerender.query-children/greenwood.config.js b/packages/plugin-graphql/test/cases/loaders-prerender.query-children/greenwood.config.js similarity index 100% rename from packages/plugin-graphql/test/cases/exp-prerender.query-children/greenwood.config.js rename to packages/plugin-graphql/test/cases/loaders-prerender.query-children/greenwood.config.js diff --git a/packages/plugin-graphql/test/cases/exp-prerender.query-children/exp-prerender.query-children.spec.js b/packages/plugin-graphql/test/cases/loaders-prerender.query-children/loaders-prerender.query-children.spec.js similarity index 100% rename from packages/plugin-graphql/test/cases/exp-prerender.query-children/exp-prerender.query-children.spec.js rename to packages/plugin-graphql/test/cases/loaders-prerender.query-children/loaders-prerender.query-children.spec.js diff --git a/packages/plugin-graphql/test/cases/exp-prerender.query-children/package.json b/packages/plugin-graphql/test/cases/loaders-prerender.query-children/package.json similarity index 100% rename from packages/plugin-graphql/test/cases/exp-prerender.query-children/package.json rename to packages/plugin-graphql/test/cases/loaders-prerender.query-children/package.json diff --git a/packages/plugin-graphql/test/cases/exp-prerender.query-children/src/components/posts-list.js b/packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/components/posts-list.js similarity index 95% rename from packages/plugin-graphql/test/cases/exp-prerender.query-children/src/components/posts-list.js rename to packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/components/posts-list.js index 6cf450ec1..32acb60d8 100644 --- a/packages/plugin-graphql/test/cases/exp-prerender.query-children/src/components/posts-list.js +++ b/packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/components/posts-list.js @@ -1,5 +1,5 @@ import client from '@greenwood/plugin-graphql/src/core/client.js'; -import ChildrenQuery from '@greenwood/plugin-graphql/src/queries/children.gql'; +import ChildrenQuery from '@greenwood/plugin-graphql/src/queries/children.gql' with { type: 'gql' }; export default class PostsList extends HTMLElement { async connectedCallback() { diff --git a/packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/blog/first-post/index.md b/packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/pages/blog/first-post/index.md similarity index 100% rename from packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/blog/first-post/index.md rename to packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/pages/blog/first-post/index.md diff --git a/packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/blog/second-post/index.md b/packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/pages/blog/second-post/index.md similarity index 100% rename from packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/blog/second-post/index.md rename to packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/pages/blog/second-post/index.md diff --git a/packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/index.html b/packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/pages/index.html similarity index 100% rename from packages/plugin-graphql/test/cases/exp-prerender.query-children/src/pages/index.html rename to packages/plugin-graphql/test/cases/loaders-prerender.query-children/src/pages/index.html diff --git a/packages/plugin-import-css/README.md b/packages/plugin-import-css/README.md index 0304e04a0..0b1d603ab 100644 --- a/packages/plugin-import-css/README.md +++ b/packages/plugin-import-css/README.md @@ -1,5 +1,7 @@ # @greenwood/plugin-import-css +> _**THIS PACKAGE HAS BEEN DEPRECATED**_ + ## Overview A Greenwood plugin to allow you use ESM (`import`) syntax to load your CSS. diff --git a/packages/plugin-import-css/package.json b/packages/plugin-import-css/package.json index 15733c402..f56454d1c 100644 --- a/packages/plugin-import-css/package.json +++ b/packages/plugin-import-css/package.json @@ -1,6 +1,7 @@ { "name": "@greenwood/plugin-import-css", "version": "0.30.0-alpha.1", + "private": true, "description": "A Greenwood plugin to allow you to use ESM (import) syntax to load your CSS.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-css", @@ -20,9 +21,6 @@ "files": [ "src/" ], - "publishConfig": { - "access": "public" - }, "peerDependencies": { "@greenwood/cli": "^0.4.0" }, diff --git a/packages/plugin-import-css/test/cases/default/default.spec.js b/packages/plugin-import-css/test/cases/default/default.spec.js index ea53973b3..c8bf0c527 100644 --- a/packages/plugin-import-css/test/cases/default/default.spec.js +++ b/packages/plugin-import-css/test/cases/default/default.spec.js @@ -35,7 +35,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; -describe('Build Greenwood With: ', function() { +xdescribe('Build Greenwood With: ', function() { const LABEL = 'Import CSS Plugin with default options'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js b/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js index 75e21bc9e..c622637c0 100644 --- a/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js @@ -30,7 +30,7 @@ import { runSmokeTest } from '../../../../../test/smoke-test.js'; const expect = chai.expect; -describe('Develop Greenwood With: ', function() { +xdescribe('Develop Greenwood With: ', function() { const LABEL = 'Import CSS plugin for using ESM with .css files'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-css/test/cases/exp-build.prerender/exp-build.prerender.spec.js b/packages/plugin-import-css/test/cases/exp-build.prerender/exp-build.prerender.spec.js index ff1a89771..659ffc324 100644 --- a/packages/plugin-import-css/test/cases/exp-build.prerender/exp-build.prerender.spec.js +++ b/packages/plugin-import-css/test/cases/exp-build.prerender/exp-build.prerender.spec.js @@ -39,7 +39,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; -describe('(Experimental) Build Greenwood With: ', function() { +xdescribe('(Experimental) Build Greenwood With: ', function() { const LABEL = 'Import CSS Plugin with static pre-rendering'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-css/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js b/packages/plugin-import-css/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js index f6a59d218..63bd0373d 100644 --- a/packages/plugin-import-css/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js +++ b/packages/plugin-import-css/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js @@ -38,7 +38,7 @@ import { fileURLToPath } from 'url'; const expect = chai.expect; -describe('Serve Greenwood With: ', function() { +xdescribe('Serve Greenwood With: ', function() { const LABEL = 'A Server Rendered Application (SSR) with API Routes importing CSS'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-json/README.md b/packages/plugin-import-json/README.md index d46f15169..d9b9cd077 100644 --- a/packages/plugin-import-json/README.md +++ b/packages/plugin-import-json/README.md @@ -1,5 +1,7 @@ # @greenwood/plugin-import-json +> _**THIS PACKAGE HAS BEEN DEPRECATED**_ + ## Overview A Greenwood plugin to allow you use ESM (`import`) syntax to load your JSON. diff --git a/packages/plugin-import-json/package.json b/packages/plugin-import-json/package.json index dfc168f34..4083ef1a0 100644 --- a/packages/plugin-import-json/package.json +++ b/packages/plugin-import-json/package.json @@ -1,6 +1,7 @@ { "name": "@greenwood/plugin-import-json", "version": "0.30.0-alpha.1", + "private": true, "description": "A Greenwood plugin to allow you to use ESM (import) syntax to load your JSON.", "repository": "https://github.com/ProjectEvergreen/greenwood", "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-json", @@ -20,9 +21,6 @@ "files": [ "src/" ], - "publishConfig": { - "access": "public" - }, "peerDependencies": { "@greenwood/cli": "^0.12.3" }, diff --git a/packages/plugin-import-json/test/cases/default/default.spec.js b/packages/plugin-import-json/test/cases/default/default.spec.js index fb168439a..dfd8a1aeb 100644 --- a/packages/plugin-import-json/test/cases/default/default.spec.js +++ b/packages/plugin-import-json/test/cases/default/default.spec.js @@ -37,7 +37,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; -describe('Build Greenwood With: ', function() { +xdescribe('Build Greenwood With: ', function() { const LABEL = 'Import JSON Plugin with default options'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js b/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js index a0b298cdc..b96adc6c1 100644 --- a/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js @@ -31,7 +31,7 @@ import { runSmokeTest } from '../../../../../test/smoke-test.js'; const expect = chai.expect; -describe('Develop Greenwood With: ', function() { +xdescribe('Develop Greenwood With: ', function() { const LABEL = 'Import JSON plugin for using ESM with .json files'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-json/test/cases/exp-build.prerender/exp-build.prerender.spec.js b/packages/plugin-import-json/test/cases/exp-build.prerender/exp-build.prerender.spec.js index c33d8f616..28bd48bff 100644 --- a/packages/plugin-import-json/test/cases/exp-build.prerender/exp-build.prerender.spec.js +++ b/packages/plugin-import-json/test/cases/exp-build.prerender/exp-build.prerender.spec.js @@ -39,7 +39,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; -describe('(Experimental) Build Greenwood With: ', function() { +xdescribe('(Experimental) Build Greenwood With: ', function() { const LABEL = 'Import JSON Plugin with static pre-rendering'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-json/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js b/packages/plugin-import-json/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js index 770c4c163..bc4b70af5 100644 --- a/packages/plugin-import-json/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js +++ b/packages/plugin-import-json/test/cases/exp-serve.ssr/exp-serve.ssr.spec.js @@ -35,7 +35,7 @@ import { fileURLToPath } from 'url'; const expect = chai.expect; -describe('Serve Greenwood With: ', function() { +xdescribe('Serve Greenwood With: ', function() { const LABEL = 'A Server Rendered Application (SSR) with API Routes importing JSON'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-jsx/README.md b/packages/plugin-import-jsx/README.md index 9de639612..f9dab1e06 100644 --- a/packages/plugin-import-jsx/README.md +++ b/packages/plugin-import-jsx/README.md @@ -51,11 +51,10 @@ export default class FooterComponent extends HTMLElement { customElements.define('app-footer', FooterComponent); ``` -A couple notes: +### Notes + - For SSR and `prerender` use cases, [follow these steps](/docs/server-rendering/#custom-imports-experimental) -- For client side / browser code specifically, it is recommended to append `?type=jsx`, e.g. +- For client side / browser code specifically, it is recommended to use import attributes syntax, e.g. ```js - import '../path/to/footer.jsx?type=jsx'; - ``` - -> _The plan is to coalesce around [import assertions](https://github.com/ProjectEvergreen/greenwood/issues/923) in time for the v1.0 release so the same standard syntax can be used on the client and the server._ \ No newline at end of file + import '../path/to/footer.jsx' with { type: 'jsx' }; + ``` \ No newline at end of file diff --git a/packages/plugin-import-jsx/package.json b/packages/plugin-import-jsx/package.json index b51e9d4cb..6c8803ac3 100644 --- a/packages/plugin-import-jsx/package.json +++ b/packages/plugin-import-jsx/package.json @@ -27,7 +27,7 @@ "@greenwood/cli": "^0.28.0-alpha.4" }, "dependencies": { - "wc-compiler": "~0.12.1" + "wc-compiler": "~0.13.0" }, "devDependencies": { "@greenwood/cli": "^0.30.0-alpha.1" diff --git a/packages/plugin-import-jsx/src/index.js b/packages/plugin-import-jsx/src/index.js index ac02dc6c4..36b678721 100644 --- a/packages/plugin-import-jsx/src/index.js +++ b/packages/plugin-import-jsx/src/index.js @@ -15,13 +15,14 @@ class ImportJsxResource extends ResourceInterface { } async shouldServe(url) { - const { pathname } = url; + const { pathname, protocol } = url; + const ext = pathname.split('.').pop(); - return pathname.split('.').pop() === this.extensions[0] && (url.searchParams.has('type') && url.searchParams.get('type') === this.extensions[0]); + return protocol === 'file:' && ext === this.extensions[0]; } async serve(url) { - // TODO refactor when WCC refactors + // refactor when WCC refactors // https://github.com/ProjectEvergreen/wcc/issues/116 const tree = parseJsx(url); const result = escodegen.generate(tree); diff --git a/packages/plugin-import-jsx/test/cases/default/src/templates/app.html b/packages/plugin-import-jsx/test/cases/default/src/templates/app.html index 970f7016c..2d99e73e2 100644 --- a/packages/plugin-import-jsx/test/cases/default/src/templates/app.html +++ b/packages/plugin-import-jsx/test/cases/default/src/templates/app.html @@ -1,7 +1,7 @@ My Personal Website - + diff --git a/packages/plugin-import-jsx/test/cases/exp-build.prerender/greenwood.config.js b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/greenwood.config.js similarity index 100% rename from packages/plugin-import-jsx/test/cases/exp-build.prerender/greenwood.config.js rename to packages/plugin-import-jsx/test/cases/loaders-build.prerender/greenwood.config.js diff --git a/packages/plugin-import-jsx/test/cases/exp-build.prerender/exp-build.prerender.spec.js b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/loaders-build.prerender.spec.js similarity index 97% rename from packages/plugin-import-jsx/test/cases/exp-build.prerender/exp-build.prerender.spec.js rename to packages/plugin-import-jsx/test/cases/loaders-build.prerender/loaders-build.prerender.spec.js index dc6914c57..bcb0e615c 100644 --- a/packages/plugin-import-jsx/test/cases/exp-build.prerender/exp-build.prerender.spec.js +++ b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/loaders-build.prerender.spec.js @@ -27,6 +27,7 @@ * index.md * templates/ * app.html + * main.js */ import chai from 'chai'; import glob from 'glob-promise'; @@ -39,7 +40,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; -describe('(Experimental) Build Greenwood With: ', function() { +describe('Build Greenwood With: ', function() { const LABEL = 'Import JSX Plugin with static pre-rendering'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/plugin-import-jsx/test/cases/exp-build.prerender/package.json b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/package.json similarity index 100% rename from packages/plugin-import-jsx/test/cases/exp-build.prerender/package.json rename to packages/plugin-import-jsx/test/cases/loaders-build.prerender/package.json diff --git a/packages/plugin-import-jsx/test/cases/exp-build.prerender/src/components/footer.jsx b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/components/footer.jsx similarity index 100% rename from packages/plugin-import-jsx/test/cases/exp-build.prerender/src/components/footer.jsx rename to packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/components/footer.jsx diff --git a/packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/main.js b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/main.js new file mode 100644 index 000000000..52c14ef72 --- /dev/null +++ b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/main.js @@ -0,0 +1 @@ +import './components/footer.jsx' with { type: 'jsx' }; \ No newline at end of file diff --git a/packages/plugin-import-jsx/test/cases/exp-build.prerender/src/pages/index.md b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/pages/index.md similarity index 100% rename from packages/plugin-import-jsx/test/cases/exp-build.prerender/src/pages/index.md rename to packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/pages/index.md diff --git a/packages/plugin-import-jsx/test/cases/exp-build.prerender/src/templates/app.html b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/templates/app.html similarity index 61% rename from packages/plugin-import-jsx/test/cases/exp-build.prerender/src/templates/app.html rename to packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/templates/app.html index b17ccc360..636405140 100644 --- a/packages/plugin-import-jsx/test/cases/exp-build.prerender/src/templates/app.html +++ b/packages/plugin-import-jsx/test/cases/loaders-build.prerender/src/templates/app.html @@ -1,7 +1,7 @@ My Personal Website - + diff --git a/packages/plugin-import-raw/README.md b/packages/plugin-import-raw/README.md new file mode 100644 index 000000000..61c33e867 --- /dev/null +++ b/packages/plugin-import-raw/README.md @@ -0,0 +1,63 @@ +# @greenwood/plugin-import-raw + +## Overview + +A Greenwood plugin to use ESM (`import`) syntax to load any file contents as a string exported as a JavaScript module. Inspired by **webpack**'s [raw loader](https://v4.webpack.js.org/loaders/raw-loader/). + +> This package assumes you already have `@greenwood/cli` installed. + +## Installation +You can use your favorite JavaScript package manager to install this package. + +_examples:_ +```bash +# npm +npm install @greenwood/plugin-import-raw --save-dev + +# yarn +yarn add @greenwood/plugin-import-raw --dev +``` + +## Usage +Add this plugin to your _greenwood.config.js_ and spread the `export`. + +```javascript +import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; + +export default { + // ... + + plugins: [ + greenwoodPluginImportRaw() + ] +} +``` + +This will then allow you to use ESM (`import`) to include any file as an arbitrary string exported as a JavaScript module. +```js +import css from '../path/to/styles.css?type=raw'; // must be a relative path per ESM spec + +console.log(css); // h1 { color: red } +``` + +> For libraries like Material Web Components, this plugin will [resolve references to _some-file.css_ if the equivalent exists that ends in _.js_ (e.g. _styles.css.js_)](https://github.com/ProjectEvergreen/greenwood/issues/700). + +## Options + +### Matches + +Optionally, you can provide an array of "matcher" patterns for the plugin to transform custom paths, which can be useful for handling imports you can't change, like third party files in _node_modules_. + +```javascript +import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; + +export default { + plugins: [ + greenwoodPluginImportRaw({ + matches: [ + '/node_modules/some-package/dist/styles.css' + ] + }) + ] +} +``` \ No newline at end of file diff --git a/packages/plugin-import-raw/package.json b/packages/plugin-import-raw/package.json new file mode 100644 index 000000000..c6d8a568c --- /dev/null +++ b/packages/plugin-import-raw/package.json @@ -0,0 +1,31 @@ +{ + "name": "@greenwood/plugin-import-raw", + "version": "0.30.0-alpha.1", + "description": "A Greenwood plugin to allow you to use ESM (import) syntax to load any file content as a string.", + "repository": "https://github.com/ProjectEvergreen/greenwood", + "homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-raw", + "author": "Owen Buckley ", + "license": "MIT", + "keywords": [ + "Greenwood", + "Static Site Generator", + "Full Stack Web Development", + "Web Components", + "NodeJS", + "ESM" + ], + "main": "src/index.js", + "type": "module", + "files": [ + "src/" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@greenwood/cli": "^0.4.0" + }, + "devDependencies": { + "@greenwood/cli": "^0.30.0-alpha.1" + } +} diff --git a/packages/plugin-import-raw/src/index.js b/packages/plugin-import-raw/src/index.js new file mode 100644 index 000000000..499b7e382 --- /dev/null +++ b/packages/plugin-import-raw/src/index.js @@ -0,0 +1,67 @@ +/* + * + * Enables using JavaScript to import any type of file as a string using ESM syntax. + * + */ +import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; + +class ImportRawResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + + this.contentType = 'text/javascript'; + } + + async shouldResolve(url) { + const matches = (this.options.matches || []).filter(matcher => url.href.indexOf(matcher) >= 0); + + if (matches.length > 0 && !url.searchParams.has('type')) { + return true; + } + } + + async resolve(url) { + const { projectDirectory } = this.compilation.context; + const { pathname, searchParams } = url; + const params = url.searchParams.size > 0 + ? `${searchParams.toString()}&type=raw` + : 'type=raw'; + const root = pathname.startsWith('file://') + ? new URL(`file://${pathname}`).href + : pathname.startsWith('/node_modules') + ? new URL(`.${pathname}`, projectDirectory).href + : new URL(`file://${pathname}`); + const matchedUrl = new URL(`${root}?${params}`); + + return new Request(matchedUrl); + } + + async shouldIntercept(url, request) { + const matches = (this.options.matches || []).filter(matcher => url.href.indexOf(matcher) >= 0); + const type = url.searchParams.get('type'); + const dest = request.headers.get('Sec-Fetch-Dest'); + + return (url.protocol === 'file:' && type === 'raw' && dest === 'empty') || matches.length > 0; + } + + async intercept(url, request, response) { + const body = await response.text(); + const contents = `const raw = \`${body.replace(/\r?\n|\r/g, ' ').replace(/\\/g, '\\\\')}\`;\nexport default raw;`; + + return new Response(contents, { + headers: new Headers({ + 'Content-Type': this.contentType + }) + }); + } +} + +const greenwoodPluginImportRaw = (options = {}) => { + return [{ + type: 'resource', + name: 'plugin-import-raw:resource', + provider: (compilation) => new ImportRawResource(compilation, options) + }]; +}; + +export { greenwoodPluginImportRaw }; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/build.matchers/build.matchers.spec.js b/packages/plugin-import-raw/test/cases/build.matchers/build.matchers.spec.js new file mode 100644 index 000000000..8580fe09b --- /dev/null +++ b/packages/plugin-import-raw/test/cases/build.matchers/build.matchers.spec.js @@ -0,0 +1,84 @@ +/* + * Use Case + * Run Greenwood and be able to load arbitrary content as a string using ESM. + * + * User Result + * Should generate a bare bones Greenwood build without erroring when using ESM (import) as a string value. + * + * User Command + * greenwood build + * + * User Config + * import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; + * + * { + * plugins: [{ + * greenwoodPluginImportRaw({ + * matches: [ + * 'eve-container.css' + * ] + * }) + * }] + * } + * + * User Workspace + * src/ + * pages/ + * index.html + * main.js + */ +import chai from 'chai'; +import fs from 'fs'; +import glob from 'glob-promise'; +import path from 'path'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With: ', function() { + const LABEL = 'Import Raw Plugin with default options'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + before(function() { + runner.setup(outputPath, getSetupFiles(outputPath)); + runner.runCommand(cliPath, 'build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Importing a string of CSS using ESM (import)', function() { + let scripts; + + before(async function() { + scripts = await glob.promise(path.join(this.context.publicDir, '*.js')); + }); + + it('should contain one (CSS-in) JavaScript file in the output directory', function() { + expect(scripts.length).to.be.equal(1); + }); + + it('should have the expected output from importing styles.css in main.js', function() { + const contents = fs.readFileSync(scripts[0], 'utf-8'); + + expect(contents).to.contain('console.log({css:"@custom-media --screen-xs'); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + }); +}); \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/build.matchers/greenwood.config.js b/packages/plugin-import-raw/test/cases/build.matchers/greenwood.config.js new file mode 100644 index 000000000..0cc263059 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/build.matchers/greenwood.config.js @@ -0,0 +1,12 @@ +import { greenwoodPluginImportRaw } from '../../../src/index.js'; + +export default { + + plugins: [ + greenwoodPluginImportRaw({ + matches: [ + 'eve-container.css' + ] + }) + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/build.matchers/src/main.js b/packages/plugin-import-raw/test/cases/build.matchers/src/main.js new file mode 100644 index 000000000..e90390161 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/build.matchers/src/main.js @@ -0,0 +1,3 @@ +import css from '@evergreen-wc/eve-container/src/eve-container.css'; + +console.log({ css }); \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/build.matchers/src/pages/index.html b/packages/plugin-import-raw/test/cases/build.matchers/src/pages/index.html new file mode 100644 index 000000000..e58731458 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/build.matchers/src/pages/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/default/default.spec.js b/packages/plugin-import-raw/test/cases/default/default.spec.js new file mode 100644 index 000000000..3aa11e5e8 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/default/default.spec.js @@ -0,0 +1,83 @@ +/* + * Use Case + * Run Greenwood and be able to load arbitrary content as a string using ESM. + * + * User Result + * Should generate a bare bones Greenwood build without erroring when using ESM (import) as a string value. + * + * User Command + * greenwood build + * + * User Config + * import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; + * + * { + * plugins: [{ + * greenwoodPluginImportRaw() + * }] + * } + * + * User Workspace + * src/ + * pages/ + * index.html + * main.js + * styles.css + */ +import chai from 'chai'; +import fs from 'fs'; +import glob from 'glob-promise'; +import path from 'path'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With: ', function() { + const LABEL = 'Import Raw Plugin with default options'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + before(function() { + runner.setup(outputPath, getSetupFiles(outputPath)); + runner.runCommand(cliPath, 'build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Importing a string of CSS using ESM (import)', function() { + let scripts; + + before(async function() { + scripts = await glob.promise(path.join(this.context.publicDir, '*.js')); + }); + + it('should contain one (CSS-in) JavaScript file in the output directory', function() { + expect(scripts.length).to.be.equal(1); + }); + + it('should have the expected output from importing styles.css in main.js', function() { + const contents = fs.readFileSync(scripts[0], 'utf-8'); + + // TODO minify CSS-in-JS? + expect(contents).to.contain('import from styles.css: p { color: red; }"'); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/default/greenwood.config.js b/packages/plugin-import-raw/test/cases/default/greenwood.config.js new file mode 100644 index 000000000..b1c45a9a5 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/default/greenwood.config.js @@ -0,0 +1,8 @@ +import { greenwoodPluginImportRaw } from '../../../src/index.js'; + +export default { + + plugins: [ + greenwoodPluginImportRaw() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/default/src/main.js b/packages/plugin-import-raw/test/cases/default/src/main.js new file mode 100644 index 000000000..a60786bca --- /dev/null +++ b/packages/plugin-import-raw/test/cases/default/src/main.js @@ -0,0 +1,3 @@ +import stylesCss from './styles.css?type=raw'; + +document.getElementsByTagName('span')[0].innerHTML = `import from styles.css: ${stylesCss}`; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/default/src/pages/index.html b/packages/plugin-import-raw/test/cases/default/src/pages/index.html new file mode 100644 index 000000000..e58731458 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/default/src/pages/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/default/src/styles.css b/packages/plugin-import-raw/test/cases/default/src/styles.css new file mode 100644 index 000000000..e21e2c285 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/default/src/styles.css @@ -0,0 +1,3 @@ +p { + color: red; +} \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/develop.default/develop.default.spec.js b/packages/plugin-import-raw/test/cases/develop.default/develop.default.spec.js new file mode 100644 index 000000000..eb99f4e35 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/develop.default/develop.default.spec.js @@ -0,0 +1,126 @@ +/* + * Use Case + * Run Greenwood develop command with no raw plugin. + * + * User Result + * Should start the development server and render a bare bones Greenwood build and return CSS file as a string using ESM. + * + * User Command + * greenwood develop + * + * User Config + * import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; + * + * { + * plugins: [{ + * greenwoodPluginImportRaw() + * }] + * } + * + * User Workspace + * src/ + * main.css + * style.css.js + * + */ +import chai from 'chai'; +import path from 'path'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; + +const expect = chai.expect; + +describe('Develop Greenwood With: ', function() { + const LABEL = 'Import Raw plugin for using ESM with arbitrary files as strings'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://localhost'; + const port = 1984; + let runner; + + before(function() { + this.context = { + hostname: `${hostname}:${port}` + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + + before(async function() { + runner.setup(outputPath); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 5000); + + runner.runCommand(cliPath, 'develop', { async: true }); + }); + }); + + runSmokeTest(['serve'], LABEL); + + describe('Develop command with raw ESM behaviors with CSS', function() { + let response = {}; + let data; + + before(async function() { + response = await fetch(`${hostname}:${port}/main.css?type=raw`, { + headers: { + 'Sec-Fetch-Dest': 'empty' + } + }); + data = await response.text(); + }); + + it('should return a 200', function() { + expect(response.status).to.equal(200); + }); + + it('should return the correct content type', function() { + expect(response.headers.get('content-type')).to.equal('text/javascript'); + }); + + // https://github.com/ProjectEvergreen/greenwood/issues/766 + // https://unpkg.com/browse/bootstrap@4.6.1/dist/css/bootstrap.css + // https://unpkg.com/browse/font-awesome@4.7.0/css/font-awesome.css + it('should return an ECMASCript module', function() { + expect(data.replace('\n', '').replace(/ /g, '').trim()) + .to.equal('constraw=`*{background-image:url("/assets/background.jpg");font-family:\'Arial\'}.blockquote-footer::before{content:"\\\\2014\\\\00A0";}.fa-chevron-right:before{content:"\\\\f054";}`;exportdefaultraw;'); // eslint-disable-line max-len + }); + }); + + // https://github.com/ProjectEvergreen/greenwood/pull/747 + // https://unpkg.com/browse/@material/mwc-button@0.22.1/styles.css.js + xdescribe('Develop command for .css.js files behaviors (CSS in disguise)', function() { + let response = {}; + let data; + + before(async function() { + response = await fetch(`${hostname}:${port}/styles.css.js`); + data = await response.text(); + }); + + it('should return a 200', function() { + expect(response.status).to.equal(200); + }); + + it('should return the correct content type', function() { + expect(response.headers.get('content-type')).to.equal('text/javascript'); + }); + + it('should return an ECMASCript module', function() { + expect(data).to.equal('export const styles = css `.mdc-touch-target-wrapper{display:inline}`;'); + }); + }); + }); + + after(function() { + runner.stopCommand(); + runner.teardown([ + path.join(outputPath, '.greenwood') + ]); + }); +}); \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/develop.default/greenwood.config.js b/packages/plugin-import-raw/test/cases/develop.default/greenwood.config.js new file mode 100644 index 000000000..da910c5a6 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/develop.default/greenwood.config.js @@ -0,0 +1,7 @@ +import { greenwoodPluginImportRaw } from '../../../src/index.js'; + +export default { + plugins: [ + greenwoodPluginImportRaw() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/develop.default/package.json b/packages/plugin-import-raw/test/cases/develop.default/package.json new file mode 100644 index 000000000..1105aa77c --- /dev/null +++ b/packages/plugin-import-raw/test/cases/develop.default/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-plugin-import-raw-css-develop-default", + "type": "module" +} \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/develop.default/src/main.css b/packages/plugin-import-raw/test/cases/develop.default/src/main.css new file mode 100644 index 000000000..f5ae38ad4 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/develop.default/src/main.css @@ -0,0 +1,12 @@ +* { + background-image: url("/assets/background.jpg"); + font-family: 'Arial' +} + +.blockquote-footer::before { + content: "\2014\00A0"; +} + +.fa-chevron-right:before { + content: "\f054"; +} \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/develop.default/src/styles.css.js b/packages/plugin-import-raw/test/cases/develop.default/src/styles.css.js new file mode 100644 index 000000000..12bcd6b16 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/develop.default/src/styles.css.js @@ -0,0 +1 @@ +export const styles = css `.mdc-touch-target-wrapper{display:inline}`; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-build.prerender/greenwood.config.js b/packages/plugin-import-raw/test/cases/loaders-build.prerender/greenwood.config.js new file mode 100644 index 000000000..e7c9f2d37 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-build.prerender/greenwood.config.js @@ -0,0 +1,8 @@ +import { greenwoodPluginImportRaw } from '../../../src/index.js'; + +export default { + prerender: true, + plugins: [ + greenwoodPluginImportRaw() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-build.prerender/loaders-build.prerender.spec.js b/packages/plugin-import-raw/test/cases/loaders-build.prerender/loaders-build.prerender.spec.js new file mode 100644 index 000000000..ccbb5efe8 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-build.prerender/loaders-build.prerender.spec.js @@ -0,0 +1,90 @@ +/* + * Use Case + * Run Greenwood with a plugin during prerendering to be able to import arbitrary text as a string using ESM. + * + * User Result + * Should generate a static Greenwood build with CSS properly prerendered. + * + * User Command + * greenwood build + * + * User Config + * import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; + * + * { + * prerender: true, + * plugins: [{ + * greenwoodPluginImportRaw() + * }] + * } + * + * User Workspace + * src/ + * components/ + * footer.css + * footer.js + * pages/ + * index.md + * templates/ + * app.html + */ +import chai from 'chai'; +import glob from 'glob-promise'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With: ', function() { + const LABEL = 'Import Raw Plugin with static pre-rendering for CSS as a string'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + before(function() { + runner.setup(outputPath, getSetupFiles(outputPath)); + runner.runCommand(cliPath, 'build'); + }); + + runSmokeTest(['public'], LABEL); + + describe('Importing CSS as a string using ESM (import)', function() { + let dom; + let scripts; + + before(async function() { + scripts = await glob.promise(path.join(this.context.publicDir, '*.js')); + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should contain no (CSS-in) JavaScript file in the output directory', function() { + expect(scripts.length).to.be.equal(0); + }); + + it('should have the expected output from importing styles.css in index.html', function() { + const styles = dom.window.document.querySelectorAll('style'); + + // TODO minify CSS-in-JS? + expect(styles.length).to.equal(1); + expect(styles[0].textContent).to.contain('.footer { width: 90%; margin: 0 auto; padding: 0; text-align: center; }'); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-build.prerender/package.json b/packages/plugin-import-raw/test/cases/loaders-build.prerender/package.json new file mode 100644 index 000000000..606abcb6a --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-build.prerender/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-plugin-import-raw-css-build-prerender", + "type": "module" +} \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/components/footer.css b/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/components/footer.css new file mode 100644 index 000000000..9141c1418 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/components/footer.css @@ -0,0 +1 @@ +.footer { width: 90%; margin: 0 auto; padding: 0; text-align: center; } \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/components/footer.js b/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/components/footer.js new file mode 100644 index 000000000..388ad54f6 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/components/footer.js @@ -0,0 +1,25 @@ +import css from './footer.css?type=raw'; + +export default class FooterComponent extends HTMLElement { + connectedCallback() { + this.innerHTML = this.getTemplate(); + } + + getTemplate() { + const year = new Date().getFullYear(); + + return ` + + + + `; + } +} + +customElements.define('app-footer', FooterComponent); \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/pages/index.md b/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/pages/index.md new file mode 100644 index 000000000..82c330a89 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/pages/index.md @@ -0,0 +1,3 @@ +# Home Page + +Welcome to the home page! \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/templates/app.html b/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/templates/app.html new file mode 100644 index 000000000..7c394fe4b --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-build.prerender/src/templates/app.html @@ -0,0 +1,12 @@ + + + My Personal Website + + + + + + + + + \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-serve.ssr/greenwood.config.js b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/greenwood.config.js new file mode 100644 index 000000000..da910c5a6 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/greenwood.config.js @@ -0,0 +1,7 @@ +import { greenwoodPluginImportRaw } from '../../../src/index.js'; + +export default { + plugins: [ + greenwoodPluginImportRaw() + ] +}; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-serve.ssr/loaders-serve.ssr.spec.js b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/loaders-serve.ssr.spec.js new file mode 100644 index 000000000..9c6c84a5e --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/loaders-serve.ssr.spec.js @@ -0,0 +1,152 @@ +/* + * Use Case + * Run Greenwood with an API and SSR routes that import raw CSS as a string. + * + * User Result + * Should generate a Greenwood build that correctly builds and bundles all assets. + * + * User Command + * greenwood build + * + * User Config + * { + * plugins: [ + * greenwoodPluginImportRaw() + * ] + * } + * + * User Workspace + * src/ + * api/ + * fragment.js + * components/ + * card.js + * card.css + * pages/ + * products.js + * services/ + * products.js + * styles/ + * some.css + */ +import chai from 'chai'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath } from 'url'; + +const expect = chai.expect; + +describe('Serve Greenwood With: ', function() { + const LABEL = 'A Server Rendered Application (SSR) with API Routes importing raw CSS'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + const hostname = 'http://localhost:8080'; + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public'), + hostname + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + + before(async function() { + runner.setup(outputPath, getSetupFiles(outputPath)); + runner.runCommand(cliPath, 'build'); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + + runner.runCommand(cliPath, 'serve', { async: true }); + }); + }); + + describe('Serve command with HTML route response for products page', function() { + let response = {}; + let data; + let productsPageDom; + + before(async function() { + response = await fetch(`${hostname}/products/`); + data = await response.text(); + productsPageDom = new JSDOM(data); + }); + + it('should return a 200 status', function() { + expect(response.status).to.equal(200); + }); + + it('should return the correct content type', function() { + expect(response.headers.get('content-type')).to.equal('text/html'); + }); + + it('should return a response body', function() { + expect(data).to.not.be.undefined; + }); + + it('should have the expected import CSS in the page in the response body', function(done) { + const styleTag = productsPageDom.window.document.querySelectorAll('body > style'); + + expect(styleTag.length).to.equal(1); + expect(styleTag[0].textContent.replace(/ /g, '').replace(/\n/, '')).contain('h1{color:red;}'); + done(); + }); + + it('should make sure to have the expected CSS inlined into the page for each ', function(done) { + const cardComponents = productsPageDom.window.document.querySelectorAll('body app-card'); + + expect(cardComponents.length).to.equal(2); + Array.from(cardComponents).forEach((card) => { + expect(card.innerHTML).contain('display: flex;'); + }); + done(); + }); + }); + + describe('Serve command with API specific behaviors for an HTML ("fragment") API', function() { + let response = {}; + let fragmentsApiDom; + + before(async function() { + response = await fetch(`${hostname}/api/fragment`); + const body = await response.clone().text(); + fragmentsApiDom = new JSDOM(body); + }); + + it('should return a 200 status', function() { + expect(response.status).to.equal(200); + }); + + it('should return a custom status message', function() { + expect(response.statusText).to.equal('OK'); + }); + + it('should return the correct content type', function() { + expect(response.headers.get('content-type')).to.equal('text/html'); + }); + + it('should make sure to have the expected CSS inlined into the page for each ', function(done) { + const cardComponents = fragmentsApiDom.window.document.querySelectorAll('body > app-card'); + + expect(cardComponents.length).to.equal(2); + Array.from(cardComponents).forEach((card) => { + expect(card.innerHTML).contain('display: flex;'); + }); + done(); + }); + }); + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + runner.stopCommand(); + }); + +}); \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-serve.ssr/package.json b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/package.json new file mode 100644 index 000000000..31d099455 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-plugin-import-raw-css-serve-ssr", + "type": "module" +} \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/api/fragment.js b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/api/fragment.js new file mode 100644 index 000000000..af4ced829 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/api/fragment.js @@ -0,0 +1,28 @@ +import { renderFromHTML } from 'wc-compiler'; +import { getProducts } from '../services/products.js'; + +export async function handler() { + const products = await getProducts(); + const { html } = await renderFromHTML(` + ${ + products.map((product) => { + const { name, thumbnail } = product; + + return ` + + `; + }).join('') + } + `, [ + new URL('../components/card.js', import.meta.url) + ]); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/components/card.css b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/components/card.css new file mode 100644 index 000000000..db18c7c4a --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/components/card.css @@ -0,0 +1,44 @@ +div { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + border: 1px solid #818181; + width: fit-content; + border-radius: 10px; + padding: 2rem 1rem; + height: 680px; + justify-content: space-between; + background-color: #fff; + overflow-x: hidden; +} + +button { + background: var(--color-accent); + color: var(--color-white); + padding: 1rem 2rem; + border: 0; + font-size: 1rem; + border-radius: 5px; + cursor: pointer; +} + +img { + max-width: 500px; + min-width: 500px; + width: 100%; +} + +h3 { + font-size: 1.85rem; +} + +@media(max-width: 768px) { + img { + max-width: 300px; + min-width: 300px; + } + div { + height: 500px; + } +} \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/components/card.js b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/components/card.js new file mode 100644 index 000000000..5ccf5dee5 --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/components/card.js @@ -0,0 +1,31 @@ +import styles from './card.css?type=raw'; + +export default class Card extends HTMLElement { + + selectItem() { + alert(`selected item is => ${this.getAttribute('title')}!`); + } + + connectedCallback() { + if (!this.shadowRoot) { + const thumbnail = this.getAttribute('thumbnail'); + const title = this.getAttribute('title'); + const template = document.createElement('template'); + + template.innerHTML = ` + +
+

${title}

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

SSR Page (w/ WCC)

+

List of Products: ${products.length}

+ +
+ ${html} +
+ `; + } +} \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/services/products.js b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/services/products.js new file mode 100644 index 000000000..96c999dac --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/services/products.js @@ -0,0 +1,11 @@ +async function getProducts() { + return [{ + name: 'iPhone 12', + thumbnail: 'iphone-12.png' + }, { + name: 'Samsung Galaxy', + thumbnail: 'samsung-galaxy.png' + }]; +} + +export { getProducts }; \ No newline at end of file diff --git a/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/styles/some.css b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/styles/some.css new file mode 100644 index 000000000..9054080ff --- /dev/null +++ b/packages/plugin-import-raw/test/cases/loaders-serve.ssr/src/styles/some.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} \ No newline at end of file diff --git a/packages/plugin-postcss/README.md b/packages/plugin-postcss/README.md index df3ff3079..739cc2c05 100644 --- a/packages/plugin-postcss/README.md +++ b/packages/plugin-postcss/README.md @@ -32,8 +32,6 @@ export default { } ``` -> 👉 _If you are using this along with [**plugin-import-css**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-css), make sure **plugin-postcss** comes first. All non standard transformations need to come last._ - Optionally, to use your own PostCSS configuration, you'll need to create _two (2)_ config files in the root of your project, by which you can provide your own custom plugins / settings that you've installed. - _postcss.config.js_ - _postcss.config.mjs_ diff --git a/packages/plugin-postcss/src/index.js b/packages/plugin-postcss/src/index.js index ab4280257..305bb21f4 100644 --- a/packages/plugin-postcss/src/index.js +++ b/packages/plugin-postcss/src/index.js @@ -33,11 +33,13 @@ class PostCssResource extends ResourceInterface { this.contentType = ['text/css']; } - async shouldIntercept(url) { - return url.protocol === 'file:' && url.pathname.split('.').pop() === this.extensions[0]; + async shouldPreIntercept(url, request, response) { + return url.protocol === 'file:' + && url.pathname.split('.').pop() === this.extensions[0] + && (request?.headers?.get('Content-Type')?.includes('text/css') || response?.headers?.get('Content-Type')?.includes('text/css')); } - async intercept(url, request, response) { + async preIntercept(url, request, response) { const config = await getConfig(this.compilation, this.options.extendConfig); const plugins = config.plugins || []; const body = await response.text(); diff --git a/packages/plugin-renderer-lit/README.md b/packages/plugin-renderer-lit/README.md index c5f867b51..8cb8e1ba6 100644 --- a/packages/plugin-renderer-lit/README.md +++ b/packages/plugin-renderer-lit/README.md @@ -38,8 +38,9 @@ yarn add @greenwood/plugin-renderer-lit --dev 1. Please familiarize yourself with some of the [caveats](https://lit.dev/docs/ssr/overview/#library-status) called out in the Lit docs, like: - Lit SSR [**only** renders into declarative shadow roots](https://github.com/lit/lit/issues/3080#issuecomment-1165158794), so you will have to keep browser support and polyfill usage in mind. - At this time, `LitElement` does not support `async` work. You can follow along with this issue [in the Lit repo](https://github.com/lit/lit/issues/2469). -1. Lit only supports templates on the server side for HTML only generated content, thus Greenwood's `getBody` API must be used. We would love for [server only components](https://github.com/lit/lit/issues/2469#issuecomment-1759583861) to be a thing though! -1. Full hydration support is not available yet. See [this Greenwood issue](https://github.com/ProjectEvergreen/greenwood/issues/880) to follow along when it will land +1. Lit only supports templates on the server side for HTML generated content, thus Greenwood's `getBody` API must be used. We would love for [server only components](https://github.com/lit/lit/issues/2469#issuecomment-1759583861) to be a thing though! +1. Lit does not support [`CSSStyleSheet` (aka CSS Modules) in their SSR DOM shim](https://github.com/lit/lit/issues/2631#issuecomment-1065400805). +1. Full hydration support is not available yet. See [this Greenwood issue](https://github.com/ProjectEvergreen/greenwood/issues/880) to follow along with when it will land. > See [this repo](https://github.com/thescientist13/greenwood-lit-ssr) for a full demo of isomorphic Lit SSR with SSR pages and API routes deployed to Vercel serverless functions. diff --git a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/build.prerender.getting-started.spec.js b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/build.prerender.getting-started.spec.js index 34ac418e6..4c5455bcb 100644 --- a/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/build.prerender.getting-started.spec.js +++ b/packages/plugin-renderer-lit/test/cases/build.prerender.getting-started/build.prerender.getting-started.spec.js @@ -10,7 +10,7 @@ * greenwood build * * User Config - * import { greenwoodPluginIncludeHTML } from '@greenwood/plugin-include-html'; + * import { greenwoodPluginRendererLit } from '@greenwood/plugin-renderer-lit'; * * { * plugins: [{ @@ -166,7 +166,7 @@ describe('Build Greenwood With Custom Lit Renderer for SSG prerendering: ', func dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); }); - it('should have expected footer

tag content in the ', function() { + it('should have no script tags in the ', function() { const scripTags = dom.window.document.querySelectorAll('body script'); expect(scripTags.length).to.be.equal(0); @@ -182,7 +182,7 @@ describe('Build Greenwood With Custom Lit Renderer for SSG prerendering: ', func body = dom.window.document.querySelector('body'); }); - it('should have expected footer

tag content in the ', function() { + it('should have expected

tag content in the
', function() { const html = body.innerHTML.trim(); expect(html).to.contain('
'); diff --git a/packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/greenwood.config.js b/packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/greenwood.config.js new file mode 100644 index 000000000..4b8dff4c9 --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/greenwood.config.js @@ -0,0 +1,9 @@ +import { greenwoodPluginRendererLit } from '../../../src/index.js'; + +export default { + plugins: [ + greenwoodPluginRendererLit({ + prerender: true + }) + ] +}; \ No newline at end of file diff --git a/packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/loaders-build.prerender.import-attributes.spec.js b/packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/loaders-build.prerender.import-attributes.spec.js new file mode 100644 index 000000000..9e06e55ae --- /dev/null +++ b/packages/plugin-renderer-lit/test/cases/loaders-build.prerender.import-attributes/loaders-build.prerender.import-attributes.spec.js @@ -0,0 +1,193 @@ +/* + * Use Case + * Run Greenwood build command with a static site and only prerendering the content (no JS!) and using import attributes + * + * User Result + * Should generate a bare bones Greenwood build with correctly templated out HTML from a LitElement. + * + * User Command + * greenwood build + * + * User Config + * import { greenwoodPluginRendererLit } from '@greenwood/plugin-renderer-lit'; + * + * { + * plugins: [{ + * greenwoodPluginRendererLit({ + * prerender: true + * }) + * }] + * } + * + * User Workspace + * src/ + * components/ + * header/ + * header.js + * header.css + * nav.json + * pages/ + * index.html + */ +import chai from 'chai'; +import { JSDOM } from 'jsdom'; +import path from 'path'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { getDependencyFiles, getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +describe('Build Greenwood With Custom Lit Renderer for SSG prerendering: ', function() { + const LABEL = 'For SSG prerendering of Getting Started example'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(false, true); + }); + + describe(LABEL, function() { + + before(async function() { + const lit = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/*.js`, + `${outputPath}/node_modules/lit/` + ); + const litDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/decorators/*.js`, + `${outputPath}/node_modules/lit/decorators/` + ); + const litDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/directives/*.js`, + `${outputPath}/node_modules/lit/directives/` + ); + const litPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit/package.json`, + `${outputPath}/node_modules/lit/` + ); + const litElement = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/*.js`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/package.json`, + `${outputPath}/node_modules/lit-element/` + ); + const litElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-element/decorators/*.js`, + `${outputPath}/node_modules/lit-element/decorators/` + ); + const litHtml = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/*.js`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlNode = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/node/*.js`, + `${outputPath}/node_modules/lit-html/node/` + ); + const litHtmlNodeDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/node/directives/*.js`, + `${outputPath}/node_modules/lit-html/node/directives/` + ); + const litHtmlPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/package.json`, + `${outputPath}/node_modules/lit-html/` + ); + const litHtmlDirectives = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/directives/*.js`, + `${outputPath}/node_modules/lit-html/directives/` + ); + const litReactiveElement = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + const litReactiveElementDecorators = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/decorators/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/decorators/` + ); + const litReactiveElementPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/package.json`, + `${outputPath}/node_modules/@lit/reactive-element/` + ); + const litReactiveElementNode = await getDependencyFiles( + `${process.cwd()}/node_modules/@lit/reactive-element/node/*.js`, + `${outputPath}/node_modules/@lit/reactive-element/node/` + ); + // lit-html/node/directives/unsafe-html.js + const litHtmlSourceMap = await getDependencyFiles( + `${process.cwd()}/node_modules/lit-html/lit-html.js.map`, + `${outputPath}/node_modules/lit-html/` + ); + const trustedTypes = await getDependencyFiles( + `${process.cwd()}/node_modules/@types/trusted-types/package.json`, + `${outputPath}/node_modules/@types/trusted-types/` + ); + + runner.setup(outputPath, [ + ...getSetupFiles(outputPath), + ...lit, + ...litPackageJson, + ...litDirectives, + ...litDecorators, + ...litElementPackageJson, + ...litElement, + ...litElementDecorators, + ...litHtmlPackageJson, + ...litHtml, + ...litHtmlNode, + ...litHtmlNodeDirectives, + ...litHtmlDirectives, + ...trustedTypes, + ...litReactiveElement, + ...litReactiveElementDecorators, + ...litReactiveElementPackageJson, + ...litReactiveElementNode, + ...litHtmlSourceMap + ]); + runner.runCommand(cliPath, 'build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe(' of the page with data-gwd-opt="static" script tags removed', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should have no script tags in the ', function() { + const scripTags = dom.window.document.querySelectorAll('body script'); + + expect(scripTags.length).to.be.equal(0); + }); + }); + + describe('LitElement statically rendered into index.html', function() { + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should have expected header