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 +21 -0
- package/README.md +135 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +238 -0
- package/package.json +40 -0
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.
|
package/dist/index.d.ts
ADDED
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
|
+
}
|