Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/issue 878 ssr custom resources #992

Merged
merged 23 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
40c520b
Enhancement/issue 971 refactor bundling and optimizations (#974)
thescientist13 Sep 22, 2022
b88bc24
v0.27.0-alpha.0
thescientist13 Oct 1, 2022
3432745
experimental loaders for SSR custom resources
thescientist13 Oct 4, 2022
a8e505d
set minimum nodejs v16.x requirement
thescientist13 Oct 22, 2022
3c59e51
latest WIP
thescientist13 Nov 1, 2022
57b0767
custom SSR loaders for JSON
thescientist13 Nov 5, 2022
c584944
working test case testing for SSR prerender with import CSS plugin
thescientist13 Nov 9, 2022
6a6d31d
experimental test task and github actions
thescientist13 Nov 10, 2022
820f71a
remove demo code
thescientist13 Nov 11, 2022
36a031c
add test cases for import JSON with prerendering
thescientist13 Nov 11, 2022
dfa5d69
upgrade latest gallinago
thescientist13 Nov 11, 2022
fef9602
enable experimental testing capabilities for CI
thescientist13 Nov 11, 2022
9b3e33f
post rebase reconsilations
thescientist13 Nov 11, 2022
98edea6
align versioning
thescientist13 Nov 11, 2022
4b71b6a
experimental specs passing on windows
thescientist13 Nov 19, 2022
3c7e465
remove lint from experimental github actions
thescientist13 Nov 19, 2022
df5e5e3
refine exp test tasks
thescientist13 Nov 19, 2022
4a448cf
minor refactor
thescientist13 Nov 19, 2022
49d40ff
fix windows exp github action workflow
thescientist13 Nov 19, 2022
1621f27
increase mocha timeout for to accomodate exp test runs
thescientist13 Nov 19, 2022
7ad0e02
formatting
thescientist13 Nov 20, 2022
d013e96
updated import CSS and JSON plugin README docs
thescientist13 Nov 20, 2022
ce7ec24
SSR usage for experimental loaders
thescientist13 Nov 20, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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