Skip to content

Commit

Permalink
feat: add server plugin, support linked compiler mode
Browse files Browse the repository at this point in the history
BREAKING CHANGE: drop support for Marko 4 compiler
BREAKING CHANGE: new api to support server plugin
  • Loading branch information
DylanPiercey committed Mar 27, 2021
1 parent adaa679 commit 70c08f0
Show file tree
Hide file tree
Showing 76 changed files with 3,103 additions and 2,200 deletions.
5 changes: 5 additions & 0 deletions .eslintignore
@@ -0,0 +1,5 @@
.vscode
node_modules
coverage
dist
__snapshots__
26 changes: 0 additions & 26 deletions .eslintrc.js

This file was deleted.

36 changes: 36 additions & 0 deletions .eslintrc.json
@@ -0,0 +1,36 @@
{
"root": true,
"extends": ["eslint:recommended", "prettier"],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": false
}
},
"env": {
"browser": true,
"node": true,
"jest": true
},
"overrides": [
{
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"no-prototype-builtins": "off",
"no-constant-condition": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-var-requires": "off"
}
}
]
}
3 changes: 3 additions & 0 deletions .fixpackrc
@@ -0,0 +1,3 @@
{
"quiet": true
}
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
paths-ignore: ["**.md"]
push:
branches: ["main"]
paths-ignore: ["**.md"]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node: [12.x, 14.x, 15.x]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use node@${{ matrix.node }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Run tests
run: npm run ci:test
- name: Report code coverage
run: npm run ci:report
5 changes: 1 addition & 4 deletions .gitignore
Expand Up @@ -11,10 +11,7 @@ npm-debug.log

# Build
dist
*.tsbuildinfo

# Coverage
coverage
.nyc_output

# Test
*.actual.*
3 changes: 2 additions & 1 deletion .lintstagedrc
@@ -1,4 +1,5 @@
{
"*.ts": ["eslint -f codeframe --fix", "prettier --write"],
"*.{js,json,md}": ["prettier --write"]
"*{.js,.json,.md,.yml,rc}": ["prettier --write"],
"./{,packages/*/}package.json": ["fixpack"]
}
10 changes: 10 additions & 0 deletions .prettierrc.json
@@ -0,0 +1,10 @@
{
"overrides": [
{
"files": "*rc",
"options": {
"parser": "json"
}
}
]
}
10 changes: 0 additions & 10 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion LICENSE
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 eBay Inc. and contributors
Copyright (c) 2021 eBay Inc. and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
142 changes: 105 additions & 37 deletions README.md
Expand Up @@ -35,10 +35,11 @@ A Marko plugin for Rollup.

# Features

1. Compiles Marko templates for the browser.
1. Compiles Marko templates for the server and browser.
2. Externalizes styles to be consumed by other tools (eg: [rollup-plugin-postcss](https://github.com/egoist/rollup-plugin-postcss#readme)).
3. Can calculate browser dependencies for a page template and send only templates with components to the browser.
4. Can output a bundle which automatically initializes Marko components.
5. Can create a [_linked_](#linked-config) build for both the server and browser with automated asset management.

**Note: The Marko runtime is authored in commonjs, this means the `@rollup/plugin-commonjs` is required!**

Expand All @@ -48,7 +49,7 @@ A Marko plugin for Rollup.
npm install @marko/rollup
```

# Example Rollup config
# Basic example config

```javascript
import nodeResolve from "@rollup/plugin-node-resolve";
Expand All @@ -58,12 +59,12 @@ import marko from "@marko/rollup";
export default {
...,
plugins: [
marko(),
marko.browser(),
nodeResolve({
browser: true,
extensions: [".js", ".marko"]
}),
// NOTE: Marko 4 compiles to commonjs, this plugin is also required.
// NOTE: The Marko runtime uses commonjs so this plugin is also required.
commonjs({
extensions: [".js", ".marko"]
}),
Expand All @@ -75,71 +76,138 @@ export default {
};
```

# Top level components
Likewise, if bundling the components for the server use `marko.server()` as the plugin.

Marko was designed to send as little JavaScript to the browser as possible. One of the ways we do this is by automatically determining which templates in your app should be shipped to the browser. When rendering a template on the server, it is only necessary to bundle the styles and interactive components rendered by that template.
# Linked config

To send the minimal amount of Marko templates to the browser you can provide a Marko template directly as the `input` to Rollup with the `hydrate` option as `true`.
If you use _both_ the `server` and `browser` plugins (in a [multi rollup config setup](https://rollupjs.org/guide/en/#configuration-files:~:text=export%20an%20array)) `@marko/rollup` will go into a _linked_ mode.
In the linked mode you will have access to the [`<rollup>` tag](#rollup-tag) on the server, and the browser config
will automatically have the [`input`](https://rollupjs.org/guide/en/#input) option set.

```js
export default {
input: "./my-marko-page.marko",
```javascript
export default [{
// Config object for bundling server assets.
input: "src/your-server-entry.js",
plugins: [
marko({
hydrate: true
}),
marko.server()
...
],
...
}
]
}, {
// Config object for bundling browser assets.
plugins: [
marko.browser()
...
]
}];
```

## `<rollup>` tag

In a [linked setup](#linked-config) you have access to the `<rollup>` tag which will provide two [tag parameters](https://markojs.com/docs/syntax/#parameters) that allow you to write out the asset links for your server rendered app.

The first parameter `entry` is the generated `input` name that the server plugin gave to the browser compiler.
You can use it to find the corresponding entry chunk from rollups build.

The second parameter `output` is an array of `AssetInfo | ChunkInfo` objects with most of the same properties returned from rollup's [`generateBundle` hook](https://rollupjs.org/guide/en/#generatebundle).

```marko
<head>
<rollup|entry, output|>
$ const entryChunk = output.find(chunk => chunk.name === entry);
<for|fileName| of=entryChunk.imports>
<link rel="modulepreload" href=fileName/>
</for>
<script async type="module" src=entryChunk.fileName/>
</rollup>
</head>
```

Include Rollup's output assets on the page with the server-rendered html and the components will be automatically initialized (you don't need to call `template.render` yourself in the browser).
Ultimately it is up to you to map the chunk data (sometimes referred to as a manifest) into the `<link>`'s and `<script>`'s rendered by your application.

## Babel options (Marko 5+)
If your rollup browser config contains multiple `output` options, or you have multiple browser configs, all of the `chunks` for each `output` are passed into the `<rollup>` tag.

If you are using Marko 5 with this plugin you can manually override the Babel configuration used by passing a `babelConfig` object to the `@marko/rollup` plugin. By default Babels regular [config file resolution](https://babeljs.io/docs/en/config-files) will be used.
For example if you have an `esm` and `iife` build:

```javascript
export default {
input: "./my-marko-page.marko",
{
plugins: [
marko({
babelConfig: {
presets: ["@babel/preset-env"]
}
}),
marko.browser()
...
],
...
output: [
{ dir: 'dist/iife', format: 'iife' },
{ dir: 'dist/esm', format: 'esm' }
]
}
```

It is recommended to use [`@babel/plugin-transform-runtime`](https://babeljs.io/docs/en/babel-plugin-transform-runtime) to avoid duplicating helpers added from Babel. To share the runtime with [`rollup-plugin-babel`](https://github.com/rollup/rollup-plugin-babel) be sure to use the [`runtimeHelpers: true` option](https://github.com/rollup/rollup-plugin-babel#helpers).
we could access the assets from both builds:

## Advanced usage
```marko
<head>
<rollup|entry, iifeOutput, esmOutput|>
$ const iifeEntryChunk = iifeOutput.find(chunk => chunk.name === entry);
$ const esmEntryChunk = esmOutput.find(chunk => chunk.name === entry);
### Multiple copies of Marko
<script async type="module" src=esmEntryChunk.fileName/>
<script nomodule src=iifeEntryChunk.fileName></script>
</rollup>
</head>
```

In some cases you may want to embed multiple isolated copies of Marko on the page. Since Marko relies on some `window` properties to initialize this can cause issues. For example, by default Marko will read the server rendered hydration code from `window.$components`. In Marko you can change these `window` properties by rendering with `{ $global: { runtimeId: "MY_MARKO_RUNTIME_ID" } }` as input on the server side.
and _boom_ you now have a [`module/nomodule` setup](https://philipwalton.com/articles/using-native-javascript-modules-in-production-today/).

# Top level components

Marko was designed to send as little JavaScript to the browser as possible. One of the ways we do this is by automatically determining which templates in your app should be shipped to the browser. When rendering a template on the server, it is only necessary to bundle the styles and interactive components rendered by that template.

This plugin exposes a `runtimeId` option produces output which will automatically initialize with the same `runtimeId` you used on the server side.
To send the minimal amount of Marko templates to the browser you can provide a Marko template directly as the `input`.
This will also automatically invoke code to initialize the components in the browser, so there is no need to call
`template.render` yourself in the browser.

> Note: if you are using _linked_ plugins then the server plugin will automatically tell the browser compiler which Marko templates to load.
```js
export default {
input: "./my-marko-page.marko",
plugins: [
marko({
hydrate: true,
runtimeId: "MY_MARKO_RUNTIME_ID" // you should also provide `{ $global: { runtimeId: "MY_MARKO_RUNTIME_ID" } }` when rendering your template on the server.
}),
marko.browser(),
...
],
...
}
```

You can also set the `initComponents` to `false` if you wish to manually call `require("marko.components").init(...)`.
## Options

Both the `server` and `browser` plugins can receive the same options.

### options.babelConfig

You can manually override the Babel configuration used by passing a `babelConfig` object to the `@marko/rollup` plugin. By default Babels regular [config file resolution](https://babeljs.io/docs/en/config-files) will be used.

```javascript
marko.browser({
babelConfig: {
presets: ["@babel/preset-env"],
},
});
```

### options.runtimeId

In some cases you may want to embed multiple isolated copies of Marko on the page. Since Marko relies on some `window` properties to initialize this can cause issues. For example, by default Marko will read the server rendered hydration code from `window.$components`. In Marko you can change these `window` properties by rendering with `{ $global: { runtimeId: "MY_MARKO_RUNTIME_ID" } }` as input on the server side.

This plugin exposes a `runtimeId` option produces output that automatically sets `$global.runtimeId` on the server side and initializes properly in the browser.

```js
const runtimeId = "MY_MARKO_RUNTIME_ID";
// Make sure the `runtimeId` is the same across all of your plugins!
marko.server({ runtimeId });
marko.browser({ runtimeId });
```

## Code of Conduct

Expand Down
34 changes: 34 additions & 0 deletions components/rollup-watch.marko
@@ -0,0 +1,34 @@
import fs from "fs";
static let watcher;
static let pendingManifest;
static function getLatestManifest($global) {
if (!watcher) {
watcher = fs.watch($global.__rollupManifest).on("change", () => {
pendingManifest = undefined
getLatestManifest($global);
});
}
return pendingManifest || (pendingManifest = fs.promises
.readFile($global.__rollupManifest, "utf-8")
.then(JSON.parse)
.catch(() =>
new Promise(resolve => watcher.once("change", resolve))
.then(() => getLatestManifest($global))
));
}

$ const $global = out.global;
<await(getLatestManifest($global))>
<@then|manifest|>
$ const entries = $global.__rollupEntries || ($global.__rollupEntries = []);
$ let writtenEntries = 0;
<__flush_here_and_after__>
$ const lastWrittenEntry = writtenEntries;
$ writtenEntries = entries.length;
<for|i| from=lastWrittenEntry to=(writtenEntries - 1)>
<${input.renderBody}(entries[i], ...manifest)/>
</>
</>
</>
</>

0 comments on commit 70c08f0

Please sign in to comment.