Skip to content

Commit

Permalink
Feature/issue 972 WCC JSX import plugin (#1085)
Browse files Browse the repository at this point in the history
* initial implementation of JSX plugin with SSR prerender support

* add bundling spec for JSX import

* documentation and README.md

* clean up console logs

* fix up

* misc PR feedback
  • Loading branch information
thescientist13 authored Apr 1, 2023
1 parent 7ff06de commit cb51860
Show file tree
Hide file tree
Showing 18 changed files with 406 additions and 5 deletions.
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"remark-rehype": "^7.0.0",
"rollup": "^2.58.0",
"unified": "^9.2.0",
"wc-compiler": "~0.6.1"
"wc-compiler": "~0.7.0"
},
"devDependencies": {
"@babel/runtime": "^7.10.4",
Expand Down
61 changes: 61 additions & 0 deletions packages/plugin-import-jsx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# @greenwood/plugin-import-jsx

## Overview
Enables usage of `import` syntax for loading [JSX rendering Web Components](https://merry-caramel-524e61.netlify.app/docs/#jsx) compatible with [**WCC**](https://github.com/ProjectEvergreen/wcc). (This is _**not**_ React JSX!)

> 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-jsx --save-dev

# yarn
yarn add @greenwood/plugin-import-jsx --dev
```

## Usage
Add this plugin to your _greenwood.config.js_.

```javascript
import { greenwoodPluginImportJsx } from '@greenwood/plugin-import-jsx';

export default {
...

plugins: [
greenwoodPluginImportJsx()
]
}
```

This will then allow you to use `import` to include [WCC]() compatible JSX rendering Web Components.
```js
export default class FooterComponent extends HTMLElement {
connectedCallback() {
this.render();
}

render() {
return (
<footer>
<h4>My Blog</h4>
</footer>
);
}
}

customElements.define('app-footer', FooterComponent);
```

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=jsx`, 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._
33 changes: 33 additions & 0 deletions packages/plugin-import-jsx/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@greenwood/plugin-import-jsx",
"version": "0.28.0-alpha.4",
"description": "A Greenwood plugin to write JSX rendering Web Components compatible with WCC.",
"repository": "https://github.com/ProjectEvergreen/greenwood",
"homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-jsx",
"author": "Owen Buckley <[email protected]>",
"license": "MIT",
"keywords": [
"Greenwood",
"JSX",
"Full Stack framework",
"NodeJS",
"WCC"
],
"main": "src/index.js",
"type": "module",
"files": [
"src/"
],
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"@greenwood/cli": "^0.28.0-alpha.4"
},
"dependencies": {
"wc-compiler": "~0.7.0"
},
"devDependencies": {
"@greenwood/cli": "^0.28.0-alpha.4"
}
}
43 changes: 43 additions & 0 deletions packages/plugin-import-jsx/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
*
* Compile Web Components rendering with JSX using wc-compiler.
*
*/
import escodegen from 'escodegen';
import { parseJsx } from 'wc-compiler/src/jsx-loader.js';
import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js';

class ImportJsxResource extends ResourceInterface {
constructor(compilation, options) {
super(compilation, options);
this.extensions = ['jsx'];
this.contentType = 'text/javascript';
}

async shouldServe(url) {
const { pathname } = url;

return pathname.split('.').pop() === this.extensions[0] && (url.searchParams.has('type') && url.searchParams.get('type') === this.extensions[0]);
}

async serve(url) {
// TODO refactor when WCC refactors
// https://github.com/ProjectEvergreen/wcc/issues/116
const tree = parseJsx(url);
const result = escodegen.generate(tree);

return new Response(result, {
headers: new Headers({
'Content-Type': this.contentType
})
});
}
}

const greenwoodPluginImportJsx = (options = {}) => [{
type: 'resource',
name: 'plugin-import-jsx:resource',
provider: (compilation) => new ImportJsxResource(compilation, options)
}];

export { greenwoodPluginImportJsx };
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Use Case
* Run Greenwood with greenwoodPluginImportJsx plugin with bundling of JSX for the client side using wc-compiler.
*
* User Result
* Should generate a static Greenwood build with JSX properly bundled.
*
* User Command
* greenwood build
*
* User Config
* import { greenwoodPluginImportJsx } from '@greenwood/plugin-import-jsx';
*
* {
* plugins: [{
* greenwoodPluginImportJsx()
* }]
* }
*
* User Workspace
* package.json
* src/
* components/
* footer.jsx
* 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 JSX Plugin for client side bundling';
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('bundling JSX 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 one bundled output file in the output directory', function() {
expect(scripts.length).to.be.equal(1);
});

it('should have the expected <script> tag in the <head> for the <app-footer> component', function() {
const scripts = dom.window.document.querySelectorAll('head > script');

expect(scripts.length).to.equal(1);
});
});
});

after(function() {
runner.teardown(getOutputTeardownFiles(outputPath));
});

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

export default {
plugins: [
...greenwoodPluginImportJsx()
]
};
5 changes: 5 additions & 0 deletions packages/plugin-import-jsx/test/cases/default/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "test-plugin-import-jsx-build-default",
"type": "module",
"version": "0.27.0-alpha.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default class FooterComponent extends HTMLElement {
connectedCallback() {
this.render();
}

render() {
return (
<footer>
<h4>My Blog</h4>
</footer>
);
}
}

customElements.define('app-footer', FooterComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Home Page

Welcome to the home page!
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html>
<head>
<title>My Personal Website</title>
<script type="module" src="../components/footer.jsx?type=jsx"></script>
</head>

<body>
<page-outlet></page-outlet>
<app-footer></app-footer>
</body>

</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Use Case
* Run Greenwood with greenwoodPluginImportJsx plugin with prerendering of JSX on the server side using wc-compiler.
*
* User Result
* Should generate a static Greenwood build with JSX properly prerendered.
*
* User Command
* greenwood build
*
* User Config
* import { greenwoodPluginImportJsx } from '@greenwood/plugin-import-jsx';
*
* {
* plugins: [{
* greenwoodPluginImportJsx()
* }]
* }
*
* User Workspace
* package.json
* src/
* components/
* footer.jsx
* 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 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));
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 JSX 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 one bundled output file in the output directory', function() {
expect(scripts.length).to.be.equal(0);
});

it('should have the expected content from importing values from package.json in index.html', function() {
const headings = dom.window.document.querySelectorAll('app-footer footer h4');
const year = new Date().getFullYear();

expect(headings.length).to.equal(1);
expect(headings[0].textContent.trim()).to.equal(`My Blog - ${year}`);
});
});
});

after(function() {
runner.teardown(getOutputTeardownFiles(outputPath));
});

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

export default {
prerender: true,
plugins: [
...greenwoodPluginImportJsx()
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "test-plugin-import-jsx-build-prerender",
"type": "module",
"version": "0.27.0-alpha.0"
}
Loading

0 comments on commit cb51860

Please sign in to comment.