Skip to content

Commit 62cd79d

Browse files
authored
Merge pull request #1191 from joshunrau/legacy-js-support
add new feature to easily inject sloppy non-strict mode scripts in iFrame document head
2 parents 040b177 + 8ee993e commit 62cd79d

File tree

11 files changed

+82
-7
lines changed

11 files changed

+82
-7
lines changed

apps/playground/src/instruments/examples/interactive/Interactive-With-Legacy-Script/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { defineInstrument } from '/runtime/v1/@opendatacapture/runtime-core';
44
import { z } from '/runtime/v1/[email protected]';
55

6-
import './legacy.js';
6+
import './legacy.js?legacy';
77

88
export default defineInstrument({
99
kind: 'INTERACTIVE',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "opendatacapture",
33
"type": "module",
4-
"version": "1.11.0",
4+
"version": "1.11.1",
55
"private": true,
66
"packageManager": "[email protected]",
77
"license": "Apache-2.0",

packages/instrument-bundler/src/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function parseBuildResult(result: BuildResult): BuildOutput {
2828
`Unexpected number of exports in output file: expected '0', found '${exportsCount}'`
2929
);
3030
}
31-
return { css: cssOutput?.text, js: jsOutput.text };
31+
return { css: cssOutput?.text, js: jsOutput.text, legacyScripts: result.legacyScripts };
3232
}
3333

3434
export async function build({ inputs }: { inputs: BundlerInput[] }): Promise<BuildOutput> {

packages/instrument-bundler/src/bundle.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@ const GLOBALS = `
4242
*/
4343
export async function createBundle(output: BuildOutput, options: { minify: boolean }) {
4444
let inject = '';
45-
if (output.css) {
46-
inject = `Object.defineProperty(__exports.content, '__injectHead', { value: Object.freeze({ style: "${btoa(output.css)}" }), writable: false });`;
45+
const style = output.css ? `"${btoa(output.css)}"` : undefined;
46+
const scripts = output.legacyScripts
47+
? `[${output.legacyScripts.map((content) => `"${btoa(content)}"`).join(', ')}]`
48+
: undefined;
49+
if (style || scripts) {
50+
inject = `Object.defineProperty(__exports.content, '__injectHead', { value: Object.freeze({ scripts: ${scripts}, style: ${style} }), writable: false });`;
4751
}
4852
const bundle = `(async () => {
4953
${GLOBALS}

packages/instrument-bundler/src/plugin.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const plugin = (options: { inputs: BundlerInput[] }): Plugin => {
99
name: 'instrument-bundler-plugin',
1010
setup(build) {
1111
const namespaces = { bundle: 'bundle' };
12+
const legacyScripts: string[] = [];
1213
build.onResolve({ filter: /.*/ }, (args) => {
1314
// css @import statement
1415
if (args.kind === 'import-rule') {
@@ -29,6 +30,35 @@ export const plugin = (options: { inputs: BundlerInput[] }): Plugin => {
2930
path: args.path
3031
};
3132
});
33+
build.onLoad({ filter: /.+\?raw$/, namespace: namespaces.bundle }, (args) => {
34+
const input = resolveInput(/(.+)\?raw$/.exec(args.path)![1]!, options.inputs);
35+
if (!input) {
36+
return {
37+
errors: [
38+
{
39+
location: { file: args.path },
40+
text: `Failed to resolve '${args.path}' from input filenames: ${options.inputs.map((file) => `'${file.name}'`).join(', ')}`
41+
}
42+
]
43+
};
44+
}
45+
return { contents: input?.content, loader: 'text' };
46+
});
47+
build.onLoad({ filter: /.+\?legacy$/, namespace: namespaces.bundle }, (args) => {
48+
const input = resolveInput(/(.+)\?legacy$/.exec(args.path)![1]!, options.inputs);
49+
if (!input) {
50+
return {
51+
errors: [
52+
{
53+
location: { file: args.path },
54+
text: `Failed to resolve '${args.path}' from input filenames: ${options.inputs.map((file) => `'${file.name}'`).join(', ')}`
55+
}
56+
]
57+
};
58+
}
59+
legacyScripts.push(input.content as string);
60+
return { contents: input.content, loader: 'empty' };
61+
});
3262
build.onLoad({ filter: /^\/runtime\/v1\/.*.css$/, namespace: namespaces.bundle }, (args) => {
3363
return { contents: `@import "${args.path}";`, loader: 'css' };
3464
});
@@ -53,6 +83,9 @@ export const plugin = (options: { inputs: BundlerInput[] }): Plugin => {
5383
}
5484
return { contents, loader };
5585
});
86+
build.onEnd((result) => {
87+
result.legacyScripts = legacyScripts;
88+
});
5689
}
5790
};
5891
};

packages/instrument-bundler/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ export type BundlerInputFileExtension =
1717
export type BuildOutput = {
1818
css?: string;
1919
js: string;
20+
legacyScripts?: string[];
2021
};

packages/instrument-bundler/src/vendor/esbuild.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
/* eslint-disable no-var */
22

3+
declare module 'esbuild' {
4+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/consistent-type-definitions
5+
export interface BuildResult<ProvidedOptions extends BuildOptions = BuildOptions> {
6+
legacyScripts?: string[];
7+
}
8+
}
9+
10+
declare module 'esbuild-wasm' {
11+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/consistent-type-definitions
12+
export interface BuildResult<ProvidedOptions extends BuildOptions = BuildOptions> {
13+
legacyScripts?: string[];
14+
}
15+
}
16+
317
if (typeof window === 'undefined') {
418
var { build, transform } = await import('esbuild');
519
} else {

packages/react-core/src/components/InteractiveContent/bootstrap.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ if (!bundle) {
5252
}
5353

5454
const instrument = await evaluateInstrument(bundle);
55+
56+
/** @type {string[] | undefined} */
57+
const scripts = instrument.content.__injectHead?.scripts;
58+
scripts?.forEach((encodedScript) => {
59+
const script = document.createElement('script');
60+
script.type = 'text/javascript';
61+
script.textContent = atob(encodedScript);
62+
document.head.appendChild(script);
63+
});
64+
5565
const encodedStyle = instrument.content.__injectHead?.style;
5666
if (encodedStyle) {
5767
const style = atob(encodedStyle);

packages/runtime-core/src/types/instrument.interactive.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ declare type InteractiveInstrument<
2020
content: {
2121
/** attributes to inject in the iframe head */
2222
readonly __injectHead?: {
23+
/** an array of base64 encoded legacy scripts */
24+
readonly scripts?: readonly string[];
2325
/** base64 encoded css */
24-
readonly style: string;
26+
readonly style?: string;
2527
};
2628
render: (done: (data: TData) => void) => Promisable<void>;
2729
};

packages/schemas/src/instrument/instrument.interactive.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const $InteractiveInstrument = $ScalarInstrument.extend({
88
content: z.object({
99
__injectHead: z
1010
.object({
11-
style: z.string().readonly()
11+
scripts: z.array(z.string().readonly()).readonly().optional(),
12+
style: z.string().readonly().optional()
1213
})
1314
.optional()
1415
.readonly(),

0 commit comments

Comments
 (0)