Skip to content

Commit

Permalink
Feature/issue 878 ssr custom resources (#992)
Browse files Browse the repository at this point in the history
* Enhancement/issue 971 refactor bundling and optimizations (#974)

* add cloud IDE caveat to puppeteer renderer plugin readme (#967)

* init commit of refactoring for script tags with a src

* initial CSS optimizing

* sync optimized link tags in final output

* refactored for shared reources

* handle inline script bundling

* support serving custom resources using Greenwood plugins in Rollup configuration without needing extra rollup plugin

* non resource related Rollup plugins supported

* custom resource plugins and rollup plugins working together

* handle empty input for Rollup

* updated lock file

* handle inline style tag bundling and optimizing

* default optimization spec passing

* refactor merging app and page templates

* clarifying corrections in spec files

* inline optimization config working

* none optimization support

* none optimization support

* none and static optimization overrides

* refactor html rendering and optimizing

* refactoring and more CLI specs passing

* add missing variable

* SSR specs and optimizing resource bundling

* minor refactoring and logging

* resolving some plugin specs

* restore develop command related GraphQL specs

* custom graphql query spec

* all specs passing

* drop rollup plugin deps from import typescript plugin

* all Greenwood commands and specs passing

* restore static router with custom prerender

* restore postcss-import

* refactor shared resources to a Map and handle dupes

* restore local packages workaround for local Rollup bundling

* better monorepo Rollup facade modules detection

* switch console log

* remove console logging

* update plugin related docs

* local solution for windows support

* refactor errant object assign

* full cross platform URL support

* fix lint

* fix extra bundles when custom prerendering

* clean up stale or already tracked TODOs

* add nested head tag smoke tests

* check for app template validation for HUD display

* misc refactoring and TODOs cleanup

* restore static router (again)

* standardize passing correct reference for prerender scripts

* clean up data-gwd-opt markers from final HTML

* v0.27.0-alpha.0

* experimental loaders for SSR custom resources

* set minimum nodejs v16.x requirement

* latest WIP

* custom SSR loaders for JSON

* working test case testing for SSR prerender with import CSS plugin

* experimental test task and github actions

* remove demo code

* add test cases for import JSON with prerendering

* upgrade latest gallinago

* enable experimental testing capabilities for CI

* post rebase reconsilations

* align versioning

* experimental specs passing on windows

* remove lint from experimental github actions

* refine exp test tasks

* minor refactor

* fix windows exp github action workflow

* increase mocha timeout for to accomodate exp test runs

* formatting

* updated import CSS and JSON plugin README docs

* SSR usage for experimental loaders
  • Loading branch information
thescientist13 authored Nov 21, 2022
1 parent 4f627bc commit 04e25c4
Show file tree
Hide file tree
Showing 28 changed files with 519 additions and 53 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/ci-exp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Continuous Integration (Experimental)

on: [pull_request]

jobs:

build:
runs-on: ubuntu-18.04

strategy:
matrix:
node: [16]

steps:
- uses: actions/checkout@v1
- name: Install Chromium Library Dependencies
run: |
sh ./.github/workflows/chromium-lib-install.sh
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- name: Installing project dependencies
run: |
yarn install --frozen-lockfile && yarn lerna bootstrap
- name: Test
run: |
yarn test:exp
25 changes: 25 additions & 0 deletions .github/workflows/ci-win-exp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Continuous Integration Windows (Experimental)

on: [pull_request]

jobs:

build:
runs-on: windows-latest

strategy:
matrix:
node: [16]

steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- name: Installing project dependencies
run: |
yarn install --frozen-lockfile --network-timeout 1000000 && yarn lerna bootstrap
- name: Test
run: |
yarn test:exp:win
5 changes: 1 addition & 4 deletions .mocharc.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
const path = require('path');

module.exports = {
spec: path.join(__dirname, 'packages/**/test/**/**/**/*.spec.js'),
timeout: 60000
timeout: 90000
};
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
14.17.0
16.17.0
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"build": "cross-env __GWD_ROLLUP_MODE__=strict node . build",
"serve": "node . serve",
"develop": "node . develop",
"test": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict c8 mocha",
"test": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict c8 mocha --exclude \"./packages/**/test/cases/exp-*/**\" \"./packages/**/**/*.spec.js\"",
"test:exp": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --experimental-loader $(pwd)/test/test-loader.js ./node_modules/mocha/bin/mocha \"./packages/**/**/*.spec.js\"",
"test:exp:win": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --experimental-loader file:\\\\%cd%\\test\\test-loader.js ./node_modules/mocha/bin/mocha --exclude \"./packages/init/test/cases/**\" \"./packages/**/**/*.spec.js\"",
"test:tdd": "yarn test --watch",
"lint:js": "eslint \"*.js\" \"./packages/**/**/*.js\" \"./test/*.js\" \"./www/**/**/*.js\"",
"lint:ts": "eslint \"./packages/**/**/*.ts\"",
Expand All @@ -40,7 +42,7 @@
"cross-env": "^7.0.3",
"eslint": "^6.8.0",
"eslint-plugin-no-only-tests": "^2.6.0",
"gallinago": "^0.5.0",
"gallinago": "^0.6.0",
"glob-promise": "^3.4.0",
"jsdom": "^16.5.0",
"lerna": "^3.16.4",
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/config/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ function greenwoodResourceLoader (compilation) {
const extension = path.extname(importAsIdAsUrl);

if (extension !== '.js') {
const originalUrl = `${id}?type=${extension.replace('.', '')}`;
let contents;

for (const plugin of resourcePlugins) {
const headers = {
request: {
originalUrl: id
originalUrl
},
response: {
'content-type': plugin.contentType
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lifecycles/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const generateGraph = async (compilation) => {
}
/* ---------End Menu Query-------------------- */
} else if (isDynamic) {
const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider().workerUrl;
const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation).workerUrl;
let ssrFrontmatter;

filePath = route;
Expand Down
68 changes: 68 additions & 0 deletions packages/cli/src/loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Node ^16.17.0
import path from 'path';
import { readAndMergeConfig as initConfig } from './lifecycles/config.js';
import { URL, fileURLToPath } from 'url';

const config = await initConfig();
const plugins = config.plugins.filter(plugin => plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin).map(plugin => plugin.provider({
context: {
projectDirectory: process.cwd()
}
}));

// TODO need to polyfill original URL header instead of extensions?
function getCustomLoaderPlugins(url, body, headers) {
return plugins.filter(plugin => plugin.extensions.includes(path.extname(url)) && (plugin.shouldServe(url, body, headers) || plugin.shouldIntercept(url, body, headers)));
}

// https://nodejs.org/docs/latest-v16.x/api/esm.html#resolvespecifier-context-nextresolve
export function resolve(specifier, context, defaultResolve) {
const { baseURL } = context;

if (getCustomLoaderPlugins(specifier).length > 0) {
return {
url: new URL(specifier, baseURL).href,
shortCircuit: true
};
}

return defaultResolve(specifier, context, defaultResolve);
}

// https://nodejs.org/docs/latest-v16.x/api/esm.html#loadurl-context-nextload
export async function load(source, context, defaultLoad) {
const resourcePlugins = getCustomLoaderPlugins(source);
const extension = path.extname(source).replace('.', '');

if (resourcePlugins.length) {
const headers = {
request: {
originalUrl: `${source}?type=${extension}`,
accept: ''
}
};
let contents = '';

for (const plugin of resourcePlugins) {
if (await plugin.shouldServe(source, headers)) {
contents = (await plugin.serve(source, headers)).body || contents;
}
}

for (const plugin of resourcePlugins) {
if (await plugin.shouldIntercept(fileURLToPath(source), contents, headers)) {
contents = (await plugin.intercept(fileURLToPath(source), contents, headers)).body || contents;
}
}

// TODO better way to handle remove export default?
// https://github.com/ProjectEvergreen/greenwood/issues/948
return {
format: extension === 'json' ? 'json' : 'module',
source: extension === 'json' ? JSON.parse(contents.replace('export default ', '')) : contents,
shortCircuit: true
};
}

return defaultLoad(source, context, defaultLoad);
}
51 changes: 25 additions & 26 deletions packages/plugin-import-css/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ A Greenwood plugin to allow you use ESM (`import`) syntax to load your CSS.

> This package assumes you already have `@greenwood/cli` installed.
## Caveats

As of now, this transformation is only supported for client side (browser) code and will not run correctly in NodeJS until [support for this is introduced into Greenwood](https://github.com/ProjectEvergreen/greenwood/issues/878), or natively by NodeJS. This means it will not work when using `prerender` option with WCC.

## Installation
You can use your favorite JavaScript package manager to install this package.

Expand Down Expand Up @@ -36,36 +32,39 @@ export default {
}
```

> 👉 _If you are using this along with [**PostCSS plugin**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss), make sure **plugin-postcss** comes first! All non standard transformations need to come last._

This will then allow you use `import` to include CSS in your JavaScript files by appending `?type=css` to the end of the `import` statement.
This will then allow you to use `import` to include CSS in your JavaScript files.
```js
import cardCss from './card.css?type=css'; // must be a relative path per ESM spec
import css from '../path/to/styles.css'; // must be a relative path per ESM spec

console.log(css) // h1 { color: red }
```

> _**Note**: Due to a characteristic of using ESM with CSS, Greenwood will also try and detect `import` usage (without needing `?type=css`), but it is recommended to favor explicitness as much as possible, given this is not a standard. Also, for projects 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)._
A couple notes:
- For SSR and `prerender` use cases, [follow these steps](/docs/server-rendering/#custom-imports-experimental)
- For client side / browser code specifically, it is recommended to append `?type=css`, e.g.
```js
import css from '../path/to/styles.css?type=css';
```

### CSS @import
If you plan to use [CSS `@import` rules](https://developer.mozilla.org/en-US/docs/Web/CSS/@import) in any of the CSS you load with this plugin, then it is recommended to use our [**PostCSS plugin**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss) and make sure to add the **postcss-import** plugin to your PostCSS config files, so as to avoid [CSS bundling issues in production](https://github.com/ProjectEvergreen/greenwood/discussions/763)_. ex:
```js
// this plugin already come with @greenwood/cli, so no need to install it!
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).

// postcss.config.mjs
export default {
plugins: [
...
> _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._

(await import('postcss-import')).default
]
};

// postcss.config.js
module.exports = {
plugins: [
...
### PostCSS
If you plan to use PostCSS, then it is recommended to use our [**PostCSS plugin**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss) and make sure **plugin-postcss** comes _before_ this plugin in your _greenwood.config.js_.

require('postcss-import')
```javascript
import { greenwoodPluginPostcss } from '@greenwood/plugin-postcss';
import { greenwoodPluginImportCss } from '@greenwood/plugin-import-css';
export default {
...
plugins: [
greenwoodPluginPostcss(),
greenwoodPluginImportCss()
]
};
}
```
5 changes: 3 additions & 2 deletions packages/plugin-import-css/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/*
*
* Enables using JavaScript to import CSS files, using ESM syntax.
Expand All @@ -7,6 +6,7 @@
import fs from 'fs';
import path from 'path';
import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js';
import { pathToFileURL } from 'url';

class ImportCssResource extends ResourceInterface {
constructor(compilation, options) {
Expand Down Expand Up @@ -48,7 +48,8 @@ class ImportCssResource extends ResourceInterface {
async intercept(url, body) {
return new Promise(async (resolve, reject) => {
try {
const cssInJsBody = `const css = \`${body.replace(/\r?\n|\r/g, ' ').replace(/\\/g, '\\\\')}\`;\nexport default css;`;
const finalBody = body || await fs.promises.readFile(pathToFileURL(url), 'utf-8');
const cssInJsBody = `const css = \`${finalBody.replace(/\r?\n|\r/g, ' ').replace(/\\/g, '\\\\')}\`;\nexport default css;`;

resolve({
body: cssInJsBody,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Use Case
* Run Greenwood with pluginImportCss plugin with prerendering of CSS on the server side.
*
* 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';
*
* {
* plugins: [{
* greenwoodPluginImportCss()
* }]
* }
*
* 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('(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));
let runner;

before(async function() {
this.context = {
publicDir: path.join(outputPath, 'public')
};
runner = new Runner(false, true);
});

describe(LABEL, function() {
before(async function() {
await runner.setup(outputPath, getSetupFiles(outputPath));
await runner.runCommand(cliPath, 'build');
});

runSmokeTest(['public'], LABEL);

describe('importing CSS 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));
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { greenwoodPluginImportCss } from '../../../src/index.js';

export default {
prerender: true,
plugins: [
...greenwoodPluginImportCss()
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "test-plugin-import-css-build-prerender",
"type": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.footer { width: 90%; margin: 0 auto; padding: 0; text-align: center; }
Loading

0 comments on commit 04e25c4

Please sign in to comment.