-
Notifications
You must be signed in to change notification settings - Fork 439
Expand file tree
/
Copy pathserve-hydration.js
More file actions
167 lines (151 loc) · 6.11 KB
/
serve-hydration.js
File metadata and controls
167 lines (151 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import path from 'node:path';
import vm from 'node:vm';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { rollup } from 'rollup';
import lwcRollupPlugin from '@lwc/rollup-plugin';
import { DISABLE_STATIC_CONTENT_OPTIMIZATION, ENGINE_SERVER } from '../../helpers/options.js';
/** LWC SSR module to use when server-side rendering components. */
const lwcSsr = await (ENGINE_SERVER
? // Using import('literal') rather than import(variable) so static analysis tools work
import('@lwc/engine-server')
: import('@lwc/ssr-runtime'));
lwcSsr.setHooks({
sanitizeHtmlContent(content) {
return content;
},
});
const ROOT_DIR = path.join(import.meta.dirname, '../..');
const COMPONENT_NAME = 'x-main';
const COMPONENT_ENTRYPOINT = 'x/main/main.js';
// Like `fs.existsSync` but async
async function exists(path) {
try {
await fs.access(path);
return true;
} catch (_err) {
return false;
}
}
async function compileModule(input, targetSSR, format) {
const modulesDir = path.join(ROOT_DIR, input.slice(0, -COMPONENT_ENTRYPOINT.length));
const bundle = await rollup({
input,
plugins: [
lwcRollupPlugin({
targetSSR,
modules: [{ dir: modulesDir }],
experimentalDynamicComponent: {
loader: fileURLToPath(new URL('../../helpers/loader.js', import.meta.url)),
strict: true,
},
enableDynamicComponents: true,
enableLwcOn: true,
enableStaticContentOptimization: !DISABLE_STATIC_CONTENT_OPTIMIZATION,
experimentalDynamicDirective: true,
}),
],
external: ['lwc', '@lwc/ssr-runtime', 'test-utils', '@test/loader'], // @todo: add ssr modules for test-utils and @test/loader
onwarn(warning, warn) {
// Ignore warnings from our own Rollup plugin
if (warning.plugin !== 'rollup-plugin-lwc-compiler') {
warn(warning);
}
},
});
const { output } = await bundle.generate({
format,
name: 'Component',
globals: {
lwc: 'LWC',
'@lwc/ssr-runtime': 'LWC',
},
});
return output[0].code;
}
/**
* This function takes a path to a component definition and a config file and returns the
* SSR-generated markup for the component. It does so by compiling the component and then
* running a script in a separate JS runtime environment to render it.
*/
async function getSsrMarkup(componentEntrypoint, configPath) {
const componentIife = await compileModule(componentEntrypoint, !ENGINE_SERVER, 'iife');
// To minimize the amount of code in the generated script, ideally we'd do `import Component`
// and delegate the bundling to the loader. However, that's complicated to configure and using
// imports with vm.Script/vm.Module is still experimental, so we use an IIFE for simplicity.
// Additionally, we could import LWC, but the framework requires configuration before each test
// (setHooks/setFeatureFlagForTest), so instead we configure it once in the top-level context
// and inject it as a global variable.
const script = new vm.Script(
`(async () => {
const {default: config} = await import('./${configPath}');
${componentIife /* var Component = ... */}
return LWC.renderComponent(
'${COMPONENT_NAME}',
Component,
config.props || {},
false,
'sync'
);
})()`,
{
filename: `[SSR] ${configPath}`,
importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
}
);
return await script.runInContext(vm.createContext({ LWC: lwcSsr }));
}
async function existsUp(dir, file) {
while (true) {
if (await exists(path.join(dir, file))) return true;
dir = path.join(dir, '..');
const basename = path.basename(dir);
if (basename === '.') return false;
}
}
/**
* Hydration test `index.spec.js` files are actually config files, not spec files.
* This function wraps those configs in the test code to be executed.
*/
async function wrapHydrationTest(configPath) {
const { default: config } = await import(path.join(ROOT_DIR, configPath));
try {
config.requiredFeatureFlags?.forEach((featureFlag) => {
lwcSsr.setFeatureFlagForTest(featureFlag, true);
});
const suiteDir = path.dirname(configPath);
const componentEntrypoint = path.join(suiteDir, COMPONENT_ENTRYPOINT);
// You can add an `.only` file alongside an `index.spec.js` file to make the test focused
const onlyFileExists = await existsUp(suiteDir, '.only');
const ssrOutput = await getSsrMarkup(componentEntrypoint, configPath);
return `
import * as LWC from 'lwc';
import { runTest } from '/helpers/test-hydrate.js';
runTest(
'/${configPath}?original=1',
'/${componentEntrypoint}',
${JSON.stringify(ssrOutput) /* escape quotes */},
${onlyFileExists}
);
`;
} finally {
config.requiredFeatureFlags?.forEach((featureFlag) => {
lwcSsr.setFeatureFlagForTest(featureFlag, false);
});
}
}
/** @type {import('@web/dev-server-core').Plugin} */
export default {
async serve(ctx) {
// Hydration test "index.spec.js" files are actually just config files.
// They don't directly define the tests. Instead, when we request the file,
// we wrap it with some boilerplate. That boilerplate must include the config
// file we originally requested, so the ?original query parameter is used
// to return the file unmodified.
if (ctx.path.endsWith('.spec.js') && !ctx.query.original) {
return await wrapHydrationTest(ctx.path.slice(1)); // remove leading /
} else if (ctx.path.endsWith('/' + COMPONENT_ENTRYPOINT)) {
return await compileModule(ctx.path.slice(1) /* remove leading / */, false, 'esm');
}
},
};