Create Raw Loader Plugin for NX Angular Application Executor

Table of contents

In this article I am going to share the plugin I created for Angular Material Blocks to preview code contents from files!
TL;DR
If you are simply interested in plugin code, jump to Creating and using plugin section!
Why NX?
Before starting to work on Angular Material Blocks, I spent some days on deciding whether I should create a project using Angular CLI or NX.
While Angular CLI is great for many use-cases, including projects with multiple libraries and applications. But, NX provides many more add-ons by default. You can read about all differences here, but below are some key points which benefited me a lot while working with NX:
Generators & Executors
- While you can create similar ones using Angular schematics & builders, they lack tooling, documentation & examples. Hence, it would become difficult to find answers if you are stuck while creating generators or executors.
Building, Testing Only What is Affected
Environment variables from
.env
files- Allows usage of
process.env
if you use@nx/angular:application
or@nx/angular:browser-esbuild
, read more here.
- Allows usage of
Why raw loader plugin?
When I started working on Angular Material Blocks, I needed some functionality which would allow me to import code snippets as constants without any extra efforts.
For example, take a look at below screenshot of Badge 1 block where I show raw contents from components’ files (HTML templates, TS class contents and CSS/SCSS style contents) along with preview.
But, NX does not have a built-in mechanism to import raw contents, so I had to create a custom plugin so that I can easily import raw contents from any files (mainly HTML, TS, CSS/SCSS).
Usage pattern
Before creating the plugin, I needed to finalize the pattern how I am going to use the plugin. I prefer how vite builder supports importing asset as string like below:
import shaderString from './shader.glsl?raw'
So my goal was create a plugin named 'raw' that enables importing files as raw text content using the ?raw
query parameter syntax.
Creating and using the plugin
Please note that I am using @nx/angular:application
executor, and it supports custom ESBuild plugins, hence the code will be compatible to only @nx/angular:application
or @nx/angular:browser-esbuild
executors.
The raw-loader
plugin
Start by creating a file at plugins/raw-loader-plugin.js
with below content:
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;
Below the explanation of the above code:
setup(build) { ... }
- This is the core function of an ESBuild plugin. It takes abuild
object as an argument, which provides methods to interact with the ESBuild build process. Inside thissetup
function, we define how our plugin will handle specific types of imports.Import Resolution Hook - The
onResolve
hook intercepts any import paths ending with?raw
using the regex filter/?raw$/
Path Handling: When a
?raw
import is detected, it resolves the absolute path by checking if the path is already absolute, or joining it with the resolver directory if it's relativeNamespace Assignment: Resolved
?raw
imports are assigned to the 'raw-loader' namespace to separate them from normal module processingFile Loading Hook: The
onLoad
hook handles files in the 'raw-loader' namespace that match the?raw
filter patternRaw Content Extraction: The plugin strips the
?raw
suffix from the file path usingreplace(/\?raw$/, '')
and reads the actual file content using Node.jsreadFileSync
Text Loader: The file contents are returned with
loader: 'text'
, which tells ESBuild to treat the content as plain text rather than JavaScript/TypeScript codeUsage Result: This allows me to import the raw source code content of files as strings, bypassing normal module compilation and getting the literal file contents instead
Providing the raw-loader
plugin
To provide the plugin to your Angular application, simply add it in the plugins
array in your project’s project.json
's targets.build.options
property, read more here:
{
"targets": {
"build": {
"executor": "@nx/angular:application",
"options": {
"plugins": [
"plugins/raw-loader-plugin.js"
]
}
}
}
}
Using the raw-loader
plugin
Now, to use the plugin, simply use the query parameter ?raw
with file-extension when you import:
import deviceServiceContent from 'path/to/device.service.ts?raw';
// prints raw TS content of device.service.ts file
console.log(deviceServiceContent)
Handling TypeScript errors
If you use the raw-loader as explained above, you will start getting TS errors like below:
Cannot find module 'path/to/device.service.ts?raw' or its corresponding type declarations.
The reasons you will get above error are as below:
Missing Type Definitions: TypeScript doesn't have built-in type definitions for the
?raw
import syntax, so it cannot understand what type the imported value should beCustom Import Syntax: The
?raw
query parameter is a custom ESBuild plugin feature, not a standard TypeScript or JavaScript import mechanism that TypeScript recognizesRuntime vs Compile Time: While the ESBuild plugin could handle the
?raw
imports at build time, TypeScript's type checker runs separately and doesn't know about this custom transformationType Inference Failure: TypeScript cannot infer that
deviceServiceContent
should be of typestring
because it doesn't understand that?raw
imports return file contents as text
To handle the error or to achieve error suppression, there are 2 ways:
Adding type definition (recommended)
Adding inline comment
Adding type definition (recommended)
Create a file types.d.ts
at root of the project with below content:
declare module '*?raw' {
const content: string;
export default content;
}
Then, add types.d.ts
file in include
array of tsconfig.base.json
:
{
"include": ["types.d.ts"]
}
Do not forget to make same changes in all your applications’ and libraries’ tsconfig files!
Once you are done adding the files, you may want to close and re-open the editor to make TS compiler stop complaining.
Adding inline comment
If you do not want to create type definition, you can add inline comment just above the import statement:
// @ts-expect-error TypeScript cannot provide types for raw-loader
import deviceServiceContent from 'path/to/device.service.ts?raw';
The @ts-expect-error
directive tells TypeScript to ignore the type error on the next line, acknowledging that this specific import will work at runtime despite TypeScript's confusion.
Achieving similar functionality in Angular CLI workspace
If you are using Angular CLI workspace, and want to achieve similar functionality, like reading the raw content of any file, there is a built-in feature available with application builder from Angular version 17.
Import attribute loader customization
You can use loader
import attribute with import
statement to customize the loading behavior.
// @ts-expect-error TypeScript cannot provide types based on attributes yet
import contents from './some-file.svg' with { loader: 'text' };
TypeScript currently lacks support for type definitions derived from import attribute values. For now, you'll need to use either @ts-expect-error
/@ts-ignore
or separate type definition files (if they're consistently imported with the same loader attribute).
You can read more about it on angular.dev.
Angular Material Blocks
I am running a limited time 20% discount on Personal & Teams licenses for lifetime access on Angular Material Blocks! Do not forget to check it out and grab this deal!
Subscribe to my newsletter
Read articles from Dharmen Shah directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Dharmen Shah
Dharmen Shah
I am a 👔👨🏻💻 Front-end Developer. I like to work on 💻 Web stuff (HTML, CSS, JS), 🅰️ Angular, ⚛️ React, 🖌️ Bootstrap. I also love 🤗 to contribute to 👐 Open-Source Projects and sometime ✒️ write 📜 articles.