@@ -13,7 +13,7 @@ import { ScriptRunnerUIPlugin } from '../tabs/script-runner-ui'
1313const profile = {
1414 name : 'scriptRunnerBridge' ,
1515 displayName : 'Script configuration' ,
16- methods : [ 'execute' , 'getConfigurations' , 'selectScriptRunner' ] ,
16+ methods : [ 'execute' , 'getConfigurations' , 'selectScriptRunner' , 'getActiveRunnerLibs' ] ,
1717 events : [ 'log' , 'info' , 'warn' , 'error' ] ,
1818 icon : 'assets/img/solid-gear-circle-play.svg' ,
1919 description : 'Configure the dependencies for running scripts.' ,
@@ -28,28 +28,69 @@ const configFileName = 'remix.config.json'
2828let baseUrl = 'https://remix-project-org.github.io/script-runner-generator'
2929const customBuildUrl = 'http://localhost:4000/build' // this will be used when the server is ready
3030
31- // A helper function that transforms ESM 'import' syntax into a format executable in the browser when 'Run Script' is triggered.
32- function transformScriptForRuntime ( scriptContent : string ) : string {
33- // Injects a helper function for dynamic imports at the top of the script.
31+ /**
32+ * @description A helper function that transforms script content for runtime execution.
33+ * It handles three types of ES module 'import' statements based on code review feedback:
34+ * 1. Relative path imports (e.g., './utils'): Converts to `require()` to use Remix's original module system.
35+ * 2. Pre-bundled library imports (e.g., 'ethers'): Converts to use global `window` objects to prevent version conflicts.
36+ * 3. External/NPM package imports (e.g., 'axios'): Converts to a dynamic `import()` from a CDN.
37+ * * @param {string } scriptContent - The original TypeScript/JavaScript content.
38+ * @param {string[] } preBundledDeps - A list of pre-bundled dependency names.
39+ * @returns {string } The transformed script content ready for execution.
40+ */
41+ function transformScriptForRuntime ( scriptContent : string , preBundledDeps : string [ ] = [ ] ) : string {
42+ // Helper for dynamically importing external packages from a CDN.
3443 const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n`
3544
36- // Transforms 'import { member } from "pkg"' into 'const { member } = await dynamicImport("pkg")'.
45+ // Step 1: Transform 'import' statements
3746 let transformed = scriptContent . replace (
38- / i m p o r t \s + ( { [ \s \S ] * ?} ) \s + f r o m \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g,
39- 'const $1 = await dynamicImport("$2");'
40- )
41- // Transforms 'import Default from "pkg"'.
42- transformed = transformed . replace (
43- / i m p o r t \s + ( [ \w \d _ $ ] + ) \s + f r o m \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g,
44- 'const $1 = (await dynamicImport("$2")).default;'
45- )
46- // Transforms 'import * as name from "pkg"'.
47- transformed = transformed . replace (
48- / i m p o r t \s + \* \s + a s \s + ( [ \w \d _ $ ] + ) \s + f r o m \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g,
49- 'const $1 = await dynamicImport("$2");'
50- )
51-
52- // Wraps the entire script in an async IIFE (Immediately Invoked Function Expression) to support top-level await.
47+ / i m p o r t \s + (?: ( { [ \s \S ] * ?} ) | ( [ \w \d _ $ ] + ) | ( \* \s + a s \s + [ \w \d _ $ ] + ) ) \s + f r o m \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g,
48+ ( match , namedMembers , defaultMember , namespaceMember , pkg ) => {
49+
50+ // Case 1: Relative path import. This was a previously working feature.
51+ // By converting to `require()`, we let Remix's original script runner handle it.
52+ if ( pkg . startsWith ( './' ) || pkg . startsWith ( '../' ) ) {
53+ if ( namedMembers ) return `const ${ namedMembers } = require("${ pkg } ");`
54+ if ( defaultMember ) return `const ${ defaultMember } = require("${ pkg } ");`
55+ if ( namespaceMember ) {
56+ const alias = namespaceMember . split ( ' as ' ) [ 1 ]
57+ return `const ${ alias } = require("${ pkg } ");`
58+ }
59+ }
60+
61+ // Case 2: Pre-bundled library import (e.g., 'ethers').
62+ // Uses the global `window` object to avoid version conflicts and TDZ ReferenceErrors.
63+ if ( preBundledDeps . includes ( pkg ) ) {
64+ const libName = pkg . split ( '/' ) . pop ( )
65+ const sourceObject = `window.${ libName } `
66+ if ( namedMembers ) return `const ${ namedMembers } = ${ sourceObject } ;`
67+ if ( defaultMember ) return `const ${ defaultMember } = ${ sourceObject } ;`
68+ if ( namespaceMember ) {
69+ const alias = namespaceMember . split ( ' as ' ) [ 1 ]
70+ return `const ${ alias } = ${ sourceObject } ;`
71+ }
72+ }
73+
74+ // Case 3: External/NPM package import.
75+ // This is the new dynamic import feature for user-added packages.
76+ if ( namedMembers ) return `const ${ namedMembers } = await dynamicImport("${ pkg } ");`
77+ if ( defaultMember ) return `const ${ defaultMember } = (await dynamicImport("${ pkg } ")).default;`
78+ if ( namespaceMember ) {
79+ const alias = namespaceMember . split ( ' as ' ) [ 1 ]
80+ return `const ${ alias } = await dynamicImport("${ pkg } ");`
81+ }
82+
83+ // Fallback for any unsupported import syntax.
84+ return `// Unsupported import for: ${ pkg } `
85+ }
86+ ) ;
87+
88+ // Step 2: Remove 'export' keyword
89+ // The script runner's execution context is not a module, so 'export' is a SyntaxError.
90+ transformed = transformed . replace ( / ^ e x p o r t \s + / gm, '' )
91+
92+ // Step 3: Wrap in an async IIFE
93+ // This enables the use of top-level 'await' for dynamic imports.
5394 return `${ dynamicImportHelper } \n(async () => {\n try {\n${ transformed } \n } catch (e) { console.error('Error executing script:', e); }\n})();`
5495}
5596
@@ -86,7 +127,6 @@ export class ScriptRunnerBridgePlugin extends Plugin {
86127 await this . loadConfigurations ( )
87128 const ui : ScriptRunnerUIPlugin = new ScriptRunnerUIPlugin ( this )
88129 this . engine . register ( ui )
89-
90130 }
91131
92132 setListeners ( ) {
@@ -138,6 +178,13 @@ export class ScriptRunnerBridgePlugin extends Plugin {
138178 } )
139179 }
140180
181+ public getActiveRunnerLibs ( ) {
182+ if ( this . activeConfig && this . activeConfig . dependencies ) {
183+ return this . activeConfig . dependencies
184+ }
185+ return [ ]
186+ }
187+
141188 public getConfigurations ( ) {
142189 return this . configurations
143190 }
@@ -224,7 +271,8 @@ export class ScriptRunnerBridgePlugin extends Plugin {
224271 this . setIsLoading ( this . activeConfig . name , true )
225272
226273 // Transforms the script into an executable format using the function defined above.
227- const transformedScript = transformScriptForRuntime ( script )
274+ const preBundledDeps = this . activeConfig . dependencies . map ( dep => dep . name )
275+ const transformedScript = transformScriptForRuntime ( script , preBundledDeps )
228276
229277 console . log ( '--- [ScriptRunner] Original Script ---' )
230278 console . log ( script )
0 commit comments