-
Notifications
You must be signed in to change notification settings - Fork 136
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- While this PR is in progress, the new runtime will exist in a separate package - Before this PR is ready to be landed (compat layer has been implemented), it will replace fusion-core. To do: - [x] Static transform - [x] Compatibility layer (implement old plugin API with new runtime) This PR differs from the initial RFC in a crucial way: plugins are executed only once as opposed to every request. Why this? - Because a linchpin of the whole system is using return values for DI, having plugin functions executed per-request implies having a dynamic DI system. While theoretically possible, it ends up just being extremely complex and possibly very confusing. - You could possibly have some Frankenstein model where some things are automatically memoized at startup (e.g. provided services) to have a static DI model but a per-request plugin execution model, but I think ultimately this just becomes confusing. In this scenario, you would simulate a "request" at startup and then memoize the DI system. - Per-request execution is more of a performance footgun. Developers have to remember to hoist things into `withStartup`. Instead, I think it is better to just move per-request code into its own umbrella hook. This results in a teeny bit more boilerplate but in exchange has a much more coherent and simple execution model with better performance by default. **As originally proposed** ```js function MyPlugin() { const [a, b] = withDeps(["A", "B"]); const result = withPlugin.using("B", b + 1).using("C", "c")(Foo); const foobar = withStartup(async () => { return `Foo: ${foo.toString()} Bar: ${bar.toString()}`; }); const [serialize, hydrate] = withUniversalValue("__STATE__"); const state = __BROWSER__ ? hydrate() : { num: Math.random(), id: process.env.APP_ID }; withEndpoint("/foobar", async (ctx, next) => { ctx.body = foobar; return next(); }); withRender(() => { serialize(state); }); withWrapper((el) => <MyProvider value={state}>{el}</MyProvider>); return foobar; } ``` **As implemented in this PR** ```js function MyPlugin() { const [a, b] = withDeps(["A", "B"]); const result = withPlugin.using("B", b + 1).using("C", "c")(Foo); const foobar = withStartup(async () => { return `Foo: ${foo.toString()} Bar: ${bar.toString()}`; }); withEndpoint("/foobar", async (ctx, next) => { ctx.body = foobar; return next(); }); const [serialize, hydrate] = withUniversalValue("__STATE__"); withRenderSetup(el => { const state = __BROWSER__ ? hydrate() : { num: Math.random(), id: process.env.APP_ID }; withSSREffect(() => { serialize(state); }); return <MyProvider value={state}>{el}</MyProvider>; }); return foobar; } ```
- Loading branch information
1 parent
825fe1f
commit 608632b
Showing
50 changed files
with
1,744 additions
and
311 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
fusion-cli-tests/test/e2e/universal-values/fixture/src/main.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// @noflow | ||
import App, {createToken, withUniversalValue, withRenderSetup, withSSREffect} from 'fusion-core'; | ||
|
||
export default async function () { | ||
const app = new App('element', (el) => el); | ||
app.register(TestPlugin1); | ||
app.register(TestPlugin2); | ||
app.register(TestPlugin3); | ||
return app; | ||
} | ||
|
||
|
||
function* TestPlugin1() { | ||
const [serialize, hydrate] = withUniversalValue("__ID_1__"); | ||
if (__NODE__) { | ||
serialize(process.env.SOME_ENV_VAR1); | ||
} | ||
|
||
if (__BROWSER__) { | ||
const hydrated = hydrate(); | ||
const el = document.createElement("div"); | ||
el.id = "result1"; | ||
el.appendChild(document.createTextNode(hydrated)); | ||
document.body.appendChild(el); | ||
} | ||
} | ||
// Hack until syntax transform exposed | ||
TestPlugin1.__fplugin__ = true; | ||
|
||
|
||
function* TestPlugin2() { | ||
const [serialize, hydrate] = withUniversalValue("__ID_2__"); | ||
if (__NODE__) { | ||
serialize(process.env.SOME_ENV_VAR2); | ||
} | ||
|
||
if (__BROWSER__) { | ||
const hydrated = hydrate(); | ||
const el = document.createElement("div"); | ||
el.id = "result2"; | ||
el.appendChild(document.createTextNode(hydrated)); | ||
document.body.appendChild(el); | ||
} | ||
} | ||
// Hack until syntax transform exposed | ||
TestPlugin2.__fplugin__ = true; | ||
|
||
|
||
function* TestPlugin3() { | ||
const [serialize, hydrate] = withUniversalValue("__ID_3__"); | ||
|
||
withRenderSetup(() => { | ||
if (__NODE__) { | ||
withSSREffect(() => { | ||
serialize("baz"); | ||
}); | ||
} | ||
}); | ||
|
||
if (__BROWSER__) { | ||
const hydrated = hydrate(); | ||
const el = document.createElement("div"); | ||
el.id = "result3"; | ||
el.appendChild(document.createTextNode(hydrated)); | ||
document.body.appendChild(el); | ||
} | ||
} | ||
// Hack until syntax transform exposed | ||
TestPlugin3.__fplugin__ = true; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// @flow | ||
/* eslint-env node */ | ||
|
||
const t = require('assert'); | ||
const path = require('path'); | ||
|
||
const puppeteer = require('puppeteer'); | ||
|
||
const {cmd, start} = require('../utils.js'); | ||
|
||
const dir = path.resolve(__dirname, './fixture'); | ||
|
||
jest.setTimeout(20000); | ||
|
||
test('universal values works', async () => { | ||
await cmd(`build --dir=${dir}`); | ||
const {proc, port} = await start(`--dir=${dir}`, { | ||
env: { | ||
...process.env, | ||
SOME_ENV_VAR1: 'foo', | ||
SOME_ENV_VAR2: 'bar', | ||
}, | ||
}); | ||
|
||
const browser = await puppeteer.launch({ | ||
args: ['--no-sandbox', '--disable-setuid-sandbox'], | ||
}); | ||
const page = await browser.newPage(); | ||
|
||
page.on('error', (err) => { | ||
// $FlowFixMe | ||
t.fail(`Client-side error: ${err}`); | ||
}); | ||
|
||
page.on('pageerror', (err) => { | ||
// $FlowFixMe | ||
t.fail(`Client-side error: ${err}`); | ||
}); | ||
|
||
await page.goto(`http://localhost:${port}/`); | ||
|
||
const [result1, result2, result3] = await Promise.all([ | ||
page.waitForSelector('#result1'), | ||
page.waitForSelector('#result2'), | ||
page.waitForSelector('#result3'), | ||
]); | ||
|
||
t.equal(await page.evaluate((el) => el.textContent, result1), 'foo'); | ||
t.equal(await page.evaluate((el) => el.textContent, result2), 'bar'); | ||
t.equal(await page.evaluate((el) => el.textContent, result3), 'baz'); | ||
|
||
browser.close(); | ||
proc.kill('SIGKILL'); | ||
}, 100000); |
159 changes: 159 additions & 0 deletions
159
fusion-cli/build/babel-plugins/babel-plugin-functional-plugin-syntax/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
/* @flow */ | ||
/* eslint-env node */ | ||
|
||
const createNamedModuleVisitor = require('../babel-plugin-utils/visit-named-module'); | ||
const {addNamed} = require('@babel/helper-module-imports'); | ||
|
||
const PURE_ANNOTATION = '#__PURE__'; | ||
|
||
const yieldHooks = new Set(['withDeps', 'withPlugin', 'withStartup']); | ||
|
||
const nodeOnlyHooks = new Set([ | ||
'withMiddleware', | ||
'withSSREffect', | ||
'withEndpoint', | ||
]); | ||
|
||
const hooks = [ | ||
...yieldHooks, | ||
...nodeOnlyHooks, | ||
'withRenderSetup', | ||
'withUniversalMiddleware', | ||
'withUniversalValue', | ||
]; | ||
|
||
module.exports = pureCreatePlugin; | ||
|
||
function pureCreatePlugin(babel /*: Object */) { | ||
const t = babel.types; | ||
const visitor = createNamedModuleVisitor( | ||
t, | ||
hooks, | ||
'fusion-core', | ||
refsHandler | ||
); | ||
return { | ||
visitor: { | ||
...visitor, | ||
Program: { | ||
enter(path /*: any */, state /*: any */) { | ||
// Track functions that need to be transformed | ||
state.pluginFunctions = new Set(); | ||
}, | ||
exit(path /*: any */, state /*: any */) { | ||
for (var pluginPath of state.pluginFunctions) { | ||
addPluginDeclaration(t, pluginPath, state); | ||
} | ||
}, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
function refsHandler(t, state, refs = []) { | ||
// Iterate in reverse as refs so we move up tree in nested cases | ||
let i = refs.length; | ||
while (i--) { | ||
const refPath = refs[i]; | ||
const name = refPath.node.name; | ||
const path = refPath.parentPath; | ||
|
||
let hookPath = path; | ||
if (name === 'withPlugin') { | ||
hookPath = assertWithPlugin(t, refPath); | ||
} else if (!t.isCallExpression(path)) { | ||
throw path.buildCodeFrameError(`${name} hook must be invoked`); | ||
} | ||
|
||
const fnParent = path.getFunctionParent(); | ||
|
||
if (!fnParent) { | ||
throw path.buildCodeFrameError( | ||
`${name} hook must be invoked within a function` | ||
); | ||
} | ||
|
||
// If originally written as generator syntax already, we do not want inject yield expressions | ||
if (!fnParent.node.generator) { | ||
if (yieldHooks.has(name)) { | ||
hookPath.replaceWith(t.yieldExpression(hookPath.node)); | ||
} | ||
} | ||
if (nodeOnlyHooks.has(name)) { | ||
path.replaceWith( | ||
t.conditionalExpression( | ||
t.identifier('__NODE__'), | ||
path.node, | ||
t.unaryExpression('void', t.numericLiteral(0)) | ||
) | ||
); | ||
} | ||
|
||
state.pluginFunctions.add(fnParent); | ||
} | ||
} | ||
|
||
function addPluginDeclaration(t, fnPath, state) { | ||
if (!state.importId) { | ||
state.importId = addNamed(fnPath, 'declarePlugin', 'fusion-core'); | ||
} | ||
|
||
if (t.isArrowFunctionExpression(fnPath)) { | ||
fnPath.arrowFunctionToExpression({}); | ||
} | ||
|
||
fnPath.node.generator = true; | ||
|
||
if (!fnPath.instrumented) { | ||
fnPath.instrumented = true; | ||
if (fnPath.isFunctionDeclaration()) { | ||
// esbuild treats assignment as non-pure so assigning a magic property | ||
// would prevent tree shaking of unused plugins | ||
// https://github.com/evanw/esbuild/issues/2010 | ||
// Instead, we convert function decalarations to hoisted function expressions | ||
const declaration = t.variableDeclaration('var', [ | ||
t.variableDeclarator( | ||
fnPath.node.id, | ||
t.addComment( | ||
t.callExpression(state.importId, [t.toExpression(fnPath.node)]), | ||
'leading', | ||
PURE_ANNOTATION | ||
) | ||
), | ||
]); | ||
// Adapted from from @babel/plugin-transform-block-scoped-functions | ||
// https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-block-scoped-functions/src/index.js#L19 | ||
declaration._blockHoist = 2; | ||
fnPath.replaceWith(declaration); | ||
} else { | ||
fnPath.replaceWith( | ||
t.addComment( | ||
t.callExpression(state.importId, [fnPath.node]), | ||
'leading', | ||
PURE_ANNOTATION | ||
) | ||
); | ||
} | ||
fnPath.skip(); | ||
} | ||
} | ||
|
||
function assertWithPlugin(t, path) { | ||
let currentPath = path; | ||
while (currentPath) { | ||
const parentPath = currentPath.parentPath; | ||
if (t.isCallExpression(parentPath)) { | ||
break; | ||
} else if ( | ||
t.isMemberExpression(parentPath) && | ||
parentPath.node.property.name === 'using' && | ||
t.isCallExpression(parentPath.parentPath) | ||
) { | ||
currentPath = currentPath.parentPath.parentPath; | ||
continue; | ||
} else { | ||
throw parentPath.buildCodeFrameError('withPlugin must be invoked'); | ||
} | ||
} | ||
return currentPath.parentPath; | ||
} |
28 changes: 28 additions & 0 deletions
28
...babel-plugins/babel-plugin-functional-plugin-syntax/test/__snapshots__/index.test.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`withPlugin errors: .using without invocation 1`] = ` | ||
"unknown: withPlugin must be invoked | ||
1 | | ||
2 | import {withPlugin} from \\"fusion-core\\"; | ||
> 3 | withPlugin.using(TokenA, TokenB); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
4 | " | ||
`; | ||
|
||
exports[`withPlugin errors: bare 1`] = ` | ||
"unknown: withPlugin must be invoked | ||
1 | | ||
2 | import {withPlugin} from \\"fusion-core\\"; | ||
> 3 | withPlugin; | ||
| ^^^^^^^^^^^ | ||
4 | " | ||
`; | ||
|
||
exports[`withPlugin errors: property 1`] = ` | ||
"unknown: withPlugin must be invoked | ||
1 | | ||
2 | import {withPlugin} from \\"fusion-core\\"; | ||
> 3 | withPlugin.someProperty; | ||
| ^^^^^^^^^^^^^^^^^^^^^^^ | ||
4 | " | ||
`; |
8 changes: 8 additions & 0 deletions
8
fusion-cli/build/babel-plugins/babel-plugin-functional-plugin-syntax/test/fixtures/arrow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import {withDeps} from "fusion-core"; | ||
|
||
function getPlugin() { | ||
return () => { | ||
console.log(this); | ||
const [a] = withDeps([A]); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
...li/build/babel-plugins/babel-plugin-functional-plugin-syntax/test/fixtures/arrow-expected
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { declarePlugin as _declarePlugin } from "fusion-core"; | ||
import { withDeps } from "fusion-core"; | ||
|
||
function getPlugin() { | ||
var _this = this; | ||
|
||
return /*#__PURE__*/_declarePlugin(function* () { | ||
console.log(_this); | ||
const [a] = yield withDeps([A]); | ||
}); | ||
} |
13 changes: 13 additions & 0 deletions
13
fusion-cli/build/babel-plugins/babel-plugin-functional-plugin-syntax/test/fixtures/collision
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import {withDeps} from "fusion-core"; | ||
|
||
const _declarePlugin = "collision"; | ||
|
||
function MyPluginA() { | ||
const [a] = withDeps([A]); | ||
return a; | ||
} | ||
|
||
function MyPluginB() { | ||
const [b] = withDeps([B]); | ||
return b; | ||
} |
Oops, something went wrong.