Integrating with JavaScript

This guide explains how to integrate JavaScript into your Utopia application.

Using Import Maps

Import maps provide a modern way to manage JavaScript module dependencies. Utopia includes built-in support for import maps through the class Utopia::ImportMap class.

Installing JavaScript Libraries

Declare browser libraries as production dependencies in package.json. The web-packages.packages section selects the files that should be served and assigns their browser import names:

{
  "private": true,
  "dependencies": {
    "@socketry/syntax": "^0.6.1"
  },
  "web-packages": {
    "packages": {
      "@socketry/syntax": {
        "include": [
          "Syntax.js"
        ],
        "imports": {
          "@socketry/syntax": "Syntax.js"
        }
      }
    }
  }
}

Install the dependencies using the configured package manager, then generate the browser-facing package projection:

$ bundle exec bake web:packages:install
$ bundle exec bake web:packages:update

This installs dependencies into node_modules/ and copies only the selected browser files into public/_components/. Treat both directories as generated projections rather than authored source.

Use an immutable installation and verify the checked-in projection in CI:

$ bundle exec bake web:packages:install frozen=true
$ bundle exec bake web:packages:check

Creating the Import Map

Create a global import map in lib/my_website/import_map.rb:

require "utopia/import_map"

module MyWebsite
	IMPORT_MAP = Utopia::ImportMap.load_manifest("public/_components")
end

This loads the generated package mappings directly, so the browser import map remains synchronized with package.json.

Then load this in lib/my_website.rb:

require_relative "my_website/import_map"

Adding to Your Pages

Add it to your page template (pages/_page.xnode), using relative_to to adjust paths for the current page:

<html>
	<head>
		#{MyWebsite::IMPORT_MAP.relative_to(request.path + "/")}
	</head>
	<body>
		<!-- Your content -->
	</body>
</html>

Using the Library

Once the import map is set up, you can import and use the library in your scripts:

<script type="module">
	// <![CDATA[
	import Syntax from '@socketry/syntax';
	
	await Syntax.highlight();
	// ]]>
</script>

Inspect the generated import map with bundle exec bake web:packages:import_map:show when debugging package resolution.

See the Web Packages documentation for workspace packages, package selection, and alternative package managers.

Advanced Import Map Features

Using CDN URLs

Import maps support direct CDN imports without downloading files:

IMPORT_MAP = Utopia::ImportMap.build do |map|
	map.import("react", "https://esm.sh/react@18")
	map.import("vue", "https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js")
end

Nested Base URLs

You can organize imports from different sources using nested with(base:) blocks:

IMPORT_MAP = Utopia::ImportMap.build do |map|
	# Local components
	map.with(base: "/_components/") do |local|
		local.import "app", "./app.js"
	end
	
	# CDN imports
	map.with(base: "https://cdn.jsdelivr.net/npm/") do |cdn|
		cdn.import "lit", "lit@2.7.5/index.js"
		cdn.import "lit/decorators.js", "lit@2.7.5/decorators.js"
	end
end

Subresource Integrity

Add integrity hashes for enhanced security:

IMPORT_MAP = Utopia::ImportMap.build do |map|
	map.import("react", "https://esm.sh/react@18", integrity: "sha384-...")
end

Scoped Imports

Use scopes to resolve imports differently based on the referrer URL (the page or module location where the import is being made):

IMPORT_MAP = Utopia::ImportMap.build do |map|
	map.import("utils", "/utils.js")
	
	# When importing from any page under /admin/, use a different utils module
	map.scope("/admin/", {"utils" => "/admin/utils.js"})
end

When you're on a page at /admin/dashboard and you import "utils", it will resolve to /admin/utils.js. On other pages, it resolves to /utils.js.

Traditional JavaScript

You can also use JavaScript by embedding it directly into your HTML, or by creating a JavaScript source file and referencing that.

Embedding Code

When embedding JavaScript directly in XRB templates, wrap the code in CDATA comments to prevent XRB's parser from interpreting special characters like <, >, and &:

<html>
	<body>
		<script type="text/javascript">
			// <![CDATA[
			console.log("Hello World")
			// ]]>
		</script>
	</body>
</html>

External Script

In script.js:

console.log("Hello World")

In your HTML view:

<html>
	<body>
		<script type="text/javascript" src="script.js"></script>
	</body>
</html>