ng-js-vite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Flores
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # ng-js-vite
2
+
3
+ Vite plugin for AngularJS apps that keeps external component templates working after build.
4
+
5
+ It finds a component `templateUrl`, copies that HTML template into the final build, gives it a content hash, and rewrites the URL so AngularJS can load it in production.
6
+
7
+ If the same component also has a `styleUrl`, the CSS is inlined into the emitted template:
8
+
9
+ ```html
10
+ <style data-ng-js-vite>
11
+ /* component css */
12
+ </style>
13
+
14
+ <!-- component html -->
15
+ ```
16
+
17
+ ## Example
18
+
19
+ ```js
20
+ import angular from "angular"
21
+
22
+ const appRootComponent = {
23
+ templateUrl: "./app-root.html",
24
+ styleUrl: "./app-root.css",
25
+ controller: AppRootController,
26
+ }
27
+
28
+ angular.module("app").component("appRoot", appRootComponent)
29
+ ```
30
+
31
+ `styleUrl` is a plugin-only property. AngularJS does not load it by itself; `ng-js-vite` reads it at build time, inlines the CSS into the emitted template, and removes the original `styleUrl` from the compiled component.
32
+
33
+ With the default config, the template is emitted as something like:
34
+
35
+ ```txt
36
+ templates/app-root-a1b2c3d4.html
37
+ ```
38
+
39
+ And the compiled component points AngularJS to that generated file.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ bun add ng-js-vite
45
+ # or
46
+ npm install ng-js-vite
47
+ ```
48
+
49
+ Requires `vite@^8` and `typescript@^5`.
50
+
51
+ ## Usage
52
+
53
+ ```ts
54
+ // vite.config.ts
55
+ import { defineConfig } from "vite"
56
+ import { ngJsTemplateParser } from "ng-js-vite"
57
+
58
+ export default defineConfig({
59
+ plugins: [ngJsTemplateParser()],
60
+ })
61
+ ```
62
+
63
+ ## File Scope
64
+
65
+ For now, this plugin supports **one `templateUrl` per source file**.
66
+
67
+ If a `styleUrl` exists in that same file, it is paired with that template and inlined into it.
68
+
69
+ Recommended component shape:
70
+
71
+ ```txt
72
+ app-root.component.js
73
+ app-root.component.html
74
+ app-root.component.css
75
+ ```
76
+
77
+ The same pattern works with TypeScript. Avoid putting multiple components with different `templateUrl` values in the same `.js` or `.ts` file. Split them into separate files instead.
78
+
79
+ ## TypeScript
80
+
81
+ `styleUrl` is not part of AngularJS' built-in `IComponentOptions` type, so TypeScript will complain if you add it to a typed component definition.
82
+
83
+ Add a project-level `.d.ts` file:
84
+
85
+ ```ts
86
+ import "angular"
87
+
88
+ declare module "angular" {
89
+ interface IComponentOptions {
90
+ styleUrl?: string
91
+ }
92
+ }
93
+ ```
94
+
95
+ Make sure that `.d.ts` file is included by your `tsconfig.json`. After that, `styleUrl` can be used directly in AngularJS component options:
96
+
97
+ ```ts
98
+ import type { IComponentOptions } from "angular"
99
+
100
+ export const appRootComponent: IComponentOptions = {
101
+ templateUrl: "./app-root.html",
102
+ styleUrl: "./app-root.css",
103
+ controller: AppRootController,
104
+ }
105
+ ```
106
+
107
+ ## Options
108
+
109
+ | Option | Type | Default | Description |
110
+ | -------- | --------- | ------- | ----------- |
111
+ | `hashed` | `boolean` | `true` | Adds a content hash to emitted template filenames. |
112
+
113
+ ```ts
114
+ ngJsTemplateParser({ hashed: false })
115
+ ```
116
+
117
+ When `hashed` is disabled, templates are emitted with their original filename:
118
+
119
+ ```txt
120
+ templates/app-root.html
121
+ ```
122
+
123
+ ## Base Path
124
+
125
+ Generated template URLs respect Vite's `base` option.
126
+
127
+ If your app uses routes, keep a base tag in `index.html` so AngularJS resolves template URLs from the app root:
128
+
129
+ ```html
130
+ <base href="/">
131
+ ```
132
+
133
+ ## Roadmap
134
+
135
+ Future versions are expected to support runtime CSS isolation, so each `templateUrl` can keep its inlined `styleUrl` styles scoped to that component template.
@@ -0,0 +1,8 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ type NgJsTemplateParserOptions = {
4
+ hashed?: boolean;
5
+ };
6
+ declare function ngJsTemplateParser(params?: NgJsTemplateParserOptions): Plugin;
7
+
8
+ export { ngJsTemplateParser };
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ // index.ts
2
+ import "vite";
3
+
4
+ // src/utils/is-valid-files.ts
5
+ function isValidFiles(filename) {
6
+ if (filename.includes("node_modules")) return;
7
+ const regExp = /\.(ts|js)(\?.*)?$/;
8
+ return regExp.test(filename);
9
+ }
10
+
11
+ // src/utils/obtain-template-url.ts
12
+ var regex = /templateUrl\s*:\s*(['"])(.*?)\1/;
13
+ function obtainTemplateUrl(code) {
14
+ if (!code.includes("templateUrl")) return;
15
+ const match = code.match(regex);
16
+ if (!match) return;
17
+ const [, , templateUrl] = match;
18
+ if (templateUrl === void 0) return;
19
+ return { templateUrl };
20
+ }
21
+
22
+ // src/utils/obtain-style-url.ts
23
+ var regex2 = /styleUrl\s*:\s*(['"])(.*?)\1/;
24
+ function obtainStyleUrl(code) {
25
+ if (!code.includes("styleUrl")) return;
26
+ const match = code.match(regex2);
27
+ if (!match) return;
28
+ const [fullMatch, , styleUrl] = match;
29
+ if (styleUrl === void 0) return;
30
+ return { styleUrl, match: fullMatch };
31
+ }
32
+
33
+ // src/utils/hash-name.ts
34
+ import { createHash } from "crypto";
35
+ function createHashedName(name, content) {
36
+ const hash = createHash("sha256");
37
+ const hashStr = hash.update(content).digest("hex").slice(0, 8);
38
+ const extensionIndex = name.lastIndexOf(".");
39
+ const base = name.slice(0, extensionIndex);
40
+ const extension = name.slice(extensionIndex);
41
+ return {
42
+ key: hashStr,
43
+ value: `${base}-${hashStr}${extension}`
44
+ };
45
+ }
46
+
47
+ // index.ts
48
+ import path2 from "path";
49
+ import { readFileSync } from "fs";
50
+ import { readFile } from "fs/promises";
51
+
52
+ // src/utils/resolve-template-path.ts
53
+ import path from "path";
54
+ function resolveTemplatePath(root, id, templateUrl) {
55
+ if (templateUrl.startsWith("/")) {
56
+ return path.resolve(root, templateUrl.slice(1));
57
+ }
58
+ if (templateUrl.startsWith("./") || templateUrl.startsWith("../")) {
59
+ return path.resolve(path.dirname(id), templateUrl);
60
+ }
61
+ if (templateUrl.startsWith("src/")) {
62
+ return path.resolve(root, templateUrl);
63
+ }
64
+ return path.resolve(path.dirname(id), templateUrl);
65
+ }
66
+
67
+ // index.ts
68
+ var DEFAULT_OPTIONS = {
69
+ hashed: true
70
+ };
71
+ function ngJsTemplateParser(params) {
72
+ const templates = /* @__PURE__ */ new Map();
73
+ const options = {
74
+ ...DEFAULT_OPTIONS,
75
+ ...params
76
+ };
77
+ let base = "/";
78
+ const root = process.cwd();
79
+ return {
80
+ name: "ngJsTemplateParser",
81
+ configResolved(config) {
82
+ base = config.base.endsWith("/") ? config.base : `${config.base}/`;
83
+ },
84
+ async transform(code, id) {
85
+ if (!isValidFiles(id)) return;
86
+ const templateObj = obtainTemplateUrl(code);
87
+ if (!templateObj) return;
88
+ const styleObj = obtainStyleUrl(code);
89
+ let transformedCode = code;
90
+ const { templateUrl } = templateObj;
91
+ const templatePath = resolveTemplatePath(
92
+ root,
93
+ id,
94
+ templateUrl
95
+ );
96
+ const stylePath = styleObj ? resolveTemplatePath(root, id, styleObj.styleUrl) : void 0;
97
+ const source = await readTemplateWithInlineStyle.call(
98
+ this,
99
+ templateUrl,
100
+ templatePath,
101
+ id,
102
+ styleObj?.styleUrl,
103
+ stylePath
104
+ );
105
+ const defaultName = path2.basename(templatePath);
106
+ const hashed = createHashedName(
107
+ defaultName,
108
+ source
109
+ );
110
+ templates.set(templatePath, {
111
+ defaultName,
112
+ hashedName: hashed.value,
113
+ dir: path2.dirname(templatePath),
114
+ sourceId: id,
115
+ stylePath
116
+ });
117
+ const outputName = options.hashed ? hashed.value : defaultName;
118
+ const publicUrl = `${base}templates/${outputName}`;
119
+ transformedCode = transformedCode.replace(templateUrl, publicUrl);
120
+ if (styleObj) {
121
+ transformedCode = transformedCode.replace(styleObj.match, "ngJsViteInlineStyle: true");
122
+ }
123
+ return {
124
+ code: transformedCode,
125
+ map: null
126
+ };
127
+ },
128
+ async generateBundle() {
129
+ const emitted = /* @__PURE__ */ new Set();
130
+ for (const template of templates.values()) {
131
+ let fileName = options.hashed ? template.hashedName : template.defaultName;
132
+ fileName = fileName.replace(/^\/+/, "");
133
+ const outputPath = path2.join("templates", fileName);
134
+ if (emitted.has(outputPath)) {
135
+ this.warn(
136
+ `ngJsTemplateParser: two templates resolve to "${outputPath}" - skipping the one referenced from "${template.sourceId}". Hashed filenames are unique by design; with "hashed: false" templates that share a basename collide. Rename one of them or enable hashing.`
137
+ );
138
+ continue;
139
+ }
140
+ const sourcePath = path2.join(
141
+ template.dir,
142
+ template.defaultName
143
+ );
144
+ const source = await readTemplateWithInlineStyle.call(
145
+ this,
146
+ sourcePath,
147
+ sourcePath,
148
+ template.sourceId,
149
+ template.stylePath,
150
+ template.stylePath
151
+ );
152
+ emitted.add(outputPath);
153
+ this.emitFile({
154
+ type: "asset",
155
+ fileName: outputPath,
156
+ source
157
+ });
158
+ }
159
+ },
160
+ configureServer(server) {
161
+ server.middlewares.use((req, res, next) => {
162
+ const url = req.url;
163
+ if (!url) return next();
164
+ let pathname = new URL(url, "http://ng-js-vite.local").pathname.replace(/^\/+/, "");
165
+ const normalizedBase = base.replace(/^\/+|\/+$/g, "");
166
+ if (normalizedBase && pathname.startsWith(`${normalizedBase}/`)) {
167
+ pathname = pathname.slice(normalizedBase.length + 1);
168
+ }
169
+ const template = [...templates.values()].find(
170
+ (template2) => getTemplateRequestPath(template2, options.hashed ?? true) === pathname
171
+ );
172
+ if (!template) return next();
173
+ try {
174
+ const filePath = path2.join(
175
+ template.dir,
176
+ template.defaultName
177
+ );
178
+ const source = readTemplateWithInlineStyleSync(
179
+ filePath,
180
+ template.stylePath
181
+ );
182
+ res.statusCode = 200;
183
+ res.setHeader(
184
+ "Content-Type",
185
+ "text/html; charset=utf-8"
186
+ );
187
+ res.end(source);
188
+ } catch (error) {
189
+ next(error);
190
+ }
191
+ });
192
+ }
193
+ };
194
+ }
195
+ async function readTemplateWithInlineStyle(templateUrl, templatePath, sourceId, styleUrl, stylePath) {
196
+ let template;
197
+ try {
198
+ template = await readFile(templatePath);
199
+ } catch (error) {
200
+ this.error(
201
+ `ngJsTemplateParser: could not read template "${templateUrl}" (resolved to "${templatePath}") referenced from "${sourceId}": ${error.message}`
202
+ );
203
+ }
204
+ if (!stylePath) return template;
205
+ let style;
206
+ try {
207
+ style = await readFile(stylePath);
208
+ } catch (error) {
209
+ this.error(
210
+ `ngJsTemplateParser: could not read style "${styleUrl}" (resolved to "${stylePath}") referenced from "${sourceId}": ${error.message}`
211
+ );
212
+ }
213
+ return inlineStyle(template, style);
214
+ }
215
+ function readTemplateWithInlineStyleSync(templatePath, stylePath) {
216
+ const template = readFileSync(templatePath);
217
+ if (!stylePath) return template;
218
+ const style = readFileSync(stylePath);
219
+ return inlineStyle(template, style);
220
+ }
221
+ function inlineStyle(template, style) {
222
+ return Buffer.concat([
223
+ Buffer.from(`<style data-ng-js-vite>
224
+ `),
225
+ style,
226
+ Buffer.from(`
227
+ </style>
228
+ `),
229
+ template
230
+ ]);
231
+ }
232
+ function getTemplateRequestPath(template, hashedFiles) {
233
+ const fileName = hashedFiles ? template.hashedName : template.defaultName;
234
+ return `templates/${fileName}`;
235
+ }
236
+ export {
237
+ ngJsTemplateParser
238
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "ng-js-vite",
3
+ "version": "0.1.0",
4
+ "description": "Vite plugin that emits content-hashed AngularJS templates and inlines styleUrl CSS.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "license": "MIT",
19
+ "author": "Max Flores",
20
+ "keywords": [
21
+ "vite",
22
+ "vite-plugin",
23
+ "angularjs",
24
+ "templateUrl",
25
+ "styleUrl"
26
+ ],
27
+ "devDependencies": {
28
+ "@types/bun": "latest",
29
+ "tsup": "^8.5.1"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "test": "bun test",
34
+ "prepublishOnly": "bun run build"
35
+ },
36
+ "peerDependencies": {
37
+ "typescript": "^5",
38
+ "vite": "^8.2.2"
39
+ }
40
+ }