Quick Overview

What follows is the custom plugin I built for Angular Material Blocks that enables importing raw file contents directly for code previews.

The Short Version

If you're only after the plugin implementation, head straight to the plugin creation and usage part.

Choosing NX

Before diving into Angular Material Blocks, I devoted time to evaluating whether to scaffold the project with Angular CLI or NX.

Angular CLI handles many scenarios well, including setups with multiple libraries and applications. Yet NX brings a richer set of built-in enhancements. While the full comparison is available here, these points were especially useful during my work:

  1. Generators and Executors
  2. Running builds and tests Only for Affected Projects
  3. Extensible Plugin Architecture
  4. Environment variable support through .env files
  5. Enforced Module Boundaries

Need for a raw loader plugin

While building Angular Material Blocks, I required a way to fetch file contents as strings with minimal effort for displaying code examples.

Take the Badge 1 block as an illustration: it presents both a live preview and the unprocessed source from component files (HTML templates, TypeScript logic, and CSS/SCSS).

Screenshot of a code editor showing HTML code for a badge component. The code uses Angular directives to create a chip set that dynamically displays badges with icons based on their trend. The editor tabs for HTML, TypeScript, and SCSS files are visible.

NX doesn't offer built-in raw content importing, so I built a custom solution to read and import the literal contents of files—primarily HTML, TypeScript, and CSS/SCSS sources.

Adopted usage pattern

Before writing any code, I settled on how the plugin would be invoked. I particularly like how vite allows importing assets as strings, shown below:

import shaderString from './shader.glsl?raw'
Enter fullscreen mode Exit fullscreen mode

That approach guided my goal: design a plugin named raw that lets you import file contents as plain text by appending the ?raw query parameter.

Building the Plugin and Integrating It

The examples below rely on the @nx/angular:application executor because it exposes support for custom ESBuild plugins. As a result, the code is specific to that executor or the @nx/angular:browser-esbuild executor.

Implementing the raw-loader Plugin

Create a new file named plugins/raw-loader-plugin.js and paste the following code into it:

import { readFileSync } from 'fs';
import * as path from 'path';

const rawLoaderPlugin = {
  name: 'raw',
  setup(build) {
    build.onResolve({ filter: /\?raw$/ }, args => {
      return {
        path: path.isAbsolute(args.path)
          ? args.path
          : path.join(args.resolveDir, args.path),
        namespace: 'raw-loader',
      };
    });
    build.onLoad({ filter: /\?raw$/, namespace: 'raw-loader' }, async args => {
      return {
        contents: readFileSync(args.path.replace(/\?raw$/, '')),
        loader: 'text',
      };
    });
  },
};

module.exports = rawLoaderPlugin;

Enter fullscreen mode Exit fullscreen mode

Here is what each part of that code does:

  1. setup(build) { ... } - This method acts as the entry point for any ESBuild plugin. It receives a build object, which exposes the API used to hook into the build process. Inside setup, the plugin declares which imports it intends to intercept.

  2. Resolution Hook - The onResolve hook uses the regular expression /?raw$/ to catch any import path that terminates with the ?raw query string.

  3. Path Resolution: Once a ?raw import is found, the plugin computes the file's absolute location. If the import path is not already absolute, it combines it with the resolver's current directory.

  4. Custom Namespace: To keep these imports away from the standard module pipeline, the resolved ?raw imports are placed into the 'raw-loader' namespace.

  5. Loading Hook: The onLoad hook targets files within the 'raw-loader' namespace and filters them based on the same ?raw pattern.

  6. Content Retrieval: The plugin eliminates the ?raw suffix from the path using replace(/\?raw$/, ''), then uses Node.js's readFileSync to fetch the file's actual contents.

  7. Text Loader: By returning the content with loader: 'text', the plugin instructs ESBuild to interpret the value as plain text rather than attempting to parse it as code.

  8. Outcome: This approach enables me to import the raw source of a file directly as a string, bypassing any compilation steps and obtaining the exact file content.

Registering the raw-loader Plugin

To make the plugin available to your Angular application, add it to the plugins array found in the targets.build.options section of your project's project.json file. Further examples can be found here:

{
  "targets": {
    "build": {
      "executor": "@nx/angular:application",
      "options": {
        "plugins": [
          "plugins/raw-loader-plugin.js"
        ]
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Importing Files with the raw-loader Plugin

Once configured, add the ?raw query parameter after the file extension in your import statement to access the file's content:

import deviceServiceContent from 'path/to/device.service.ts?raw';

// prints raw TS content of device.service.ts file
console.log(deviceServiceContent)

Enter fullscreen mode Exit fullscreen mode

Addressing TypeScript Errors

Directly applying the raw-loader as described will trigger TypeScript errors similar to this:

Cannot find module 'path/to/device.service.ts?raw' or its corresponding type declarations.
Enter fullscreen mode Exit fullscreen mode

The error appears for these reasons:

  • No Built-in Types: TypeScript lacks definitions for the ?raw import syntax, leaving the type of the imported module unknown.

  • Non-standard Syntax: The ?raw suffix is a custom feature of the ESBuild plugin, which TypeScript does not recognize as a valid import mechanism.

  • Build-Time vs. Type-Checking: While ESBuild processes the ?raw imports during the build, TypeScript's type checker operates independently and is unaware of this transformation.

  • Inferred Type Failure: TypeScript cannot deduce that the variable receiving the import, such as deviceServiceContent, should be a string, because it doesn't understand that ?raw imports produce the file's text content.

Two methods are available to resolve or suppress these errors:

  1. Adding a type definition (preferred)

  2. Adding an inline comment

Using a Type Definition (Recommended)

Start by creating a types.d.ts file in the project's root and add the following declaration:

declare module '*?raw' {
  const content: string;
  export default content;
}

Enter fullscreen mode Exit fullscreen mode

Next, register the new types.d.ts file by adding it to the include array within tsconfig.base.json:

{ "include": ["types.d.ts"]}
Enter fullscreen mode Exit fullscreen mode

Remember to apply the same configuration changes to the tsconfig files of every application and library in your workspace.

After making these changes, you might need to restart your editor for the TypeScript server to recognize the new definitions and stop reporting errors.

Using an Inline Comment

If creating a type definition isn't appealing, you can insert an inline comment directly above the import statement instead:

// @ts-expect-error TypeScript cannot provide types for raw-loader
import deviceServiceContent from 'path/to/device.service.ts?raw';

Enter fullscreen mode Exit fullscreen mode

The @ts-expect-error directive is a signal to TypeScript to suppress any type error that appears on the very next line. This tells the compiler to accept that the import will function at runtime, even if it can't verify the type statically.

Replicating the same behavior in Angular CLI workspaces

For teams working in an Angular CLI workspace who need comparable functionality — such as pulling in the raw text of any file — the application builder introduced in Angular 17 ships with this capability out of the box.

Adjusting loading logic via the loader import attribute

The loader import attribute can be paired with an import statement to alter how a module is loaded.

// @ts-expect-error TypeScript cannot provide types based on attributes yet
import contents from './some-file.svg' with { loader: 'text' };

Enter fullscreen mode Exit fullscreen mode

TypeScript does not yet offer type definitions that correspond to import attribute values. Until that changes, developers will have to rely on @ts-expect-error or @ts-ignore, or maintain separate type declaration files provided the same loader attribute is used consistently across imports.

Additional details are available on angular.dev.


Angular Material Blocks

For a limited period, I am offering a 20% discount on Personal & Teams licenses with lifetime access to Angular Material Blocks! Be sure to explore it and take advantage of this offer.

Create Raw Loader Plugin for NX Angular Application Executor — figure 2