@praxisjs/vite-plugin 4.0.6 → 4.2.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/CHANGELOG.md +28 -0
- package/dist/decorators.d.ts +34 -1
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +469 -25
- package/dist/decorators.js.map +1 -1
- package/dist/hmr.d.ts +5 -0
- package/dist/hmr.d.ts.map +1 -0
- package/dist/hmr.js +106 -0
- package/dist/hmr.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -19
- package/dist/index.js.map +1 -1
- package/dist/keyed-lists.d.ts +28 -0
- package/dist/keyed-lists.d.ts.map +1 -0
- package/dist/keyed-lists.js +251 -0
- package/dist/keyed-lists.js.map +1 -0
- package/dist/libraries.d.ts +56 -0
- package/dist/libraries.d.ts.map +1 -0
- package/dist/libraries.js +281 -0
- package/dist/libraries.js.map +1 -0
- package/dist/mapped-output.d.ts +34 -0
- package/dist/mapped-output.d.ts.map +1 -0
- package/dist/mapped-output.js +119 -0
- package/dist/mapped-output.js.map +1 -0
- package/dist/templates.d.ts +56 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +708 -0
- package/dist/templates.js.map +1 -0
- package/package.json +13 -5
- package/src/decorators.ts +496 -27
- package/src/hmr.ts +108 -0
- package/src/index.ts +39 -26
- package/src/keyed-lists.ts +285 -0
- package/src/libraries.ts +328 -0
- package/src/mapped-output.ts +128 -0
- package/src/templates.ts +733 -0
- package/dist/__tests__/css-esbuild-error.test.d.ts +0 -2
- package/dist/__tests__/css-esbuild-error.test.d.ts.map +0 -1
- package/dist/__tests__/css-esbuild-error.test.js +0 -25
- package/dist/__tests__/css-esbuild-error.test.js.map +0 -1
- package/dist/__tests__/css-tsconfig-error.test.d.ts +0 -2
- package/dist/__tests__/css-tsconfig-error.test.d.ts.map +0 -1
- package/dist/__tests__/css-tsconfig-error.test.js +0 -39
- package/dist/__tests__/css-tsconfig-error.test.js.map +0 -1
- package/dist/__tests__/css-tsconfig-no-config.test.d.ts +0 -2
- package/dist/__tests__/css-tsconfig-no-config.test.d.ts.map +0 -1
- package/dist/__tests__/css-tsconfig-no-config.test.js +0 -38
- package/dist/__tests__/css-tsconfig-no-config.test.js.map +0 -1
- package/dist/__tests__/css.test.d.ts +0 -2
- package/dist/__tests__/css.test.d.ts.map +0 -1
- package/dist/__tests__/css.test.js +0 -288
- package/dist/__tests__/css.test.js.map +0 -1
- package/dist/__tests__/index.test.d.ts +0 -2
- package/dist/__tests__/index.test.d.ts.map +0 -1
- package/dist/__tests__/index.test.js +0 -301
- package/dist/__tests__/index.test.js.map +0 -1
- package/src/__tests__/css-esbuild-error.test.ts +0 -39
- package/src/__tests__/css-tsconfig-error.test.ts +0 -53
- package/src/__tests__/css-tsconfig-no-config.test.ts +0 -52
- package/src/__tests__/css.test.ts +0 -381
- package/src/__tests__/index.test.ts +0 -351
- package/tsconfig.json +0 -11
package/src/libraries.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { HELPERS_ID, helpersModule, transformDecorators } from "./decorators.js";
|
|
6
|
+
import { transformKeyedLists } from "./keyed-lists.js";
|
|
7
|
+
import { MappedOutput } from "./mapped-output.js";
|
|
8
|
+
import { transformTemplates } from "./templates.js";
|
|
9
|
+
|
|
10
|
+
import type { Plugin, Rolldown } from "vite";
|
|
11
|
+
|
|
12
|
+
// A PraxisJS library built with tsc reaches an app as plain JS: none of the build-time work the
|
|
13
|
+
// app's own files get (claimed fields, compiled templates, keyed lists) happens to it. A library
|
|
14
|
+
// opts in with a `praxisjs` field in its package.json; an app can also opt a package in or out by
|
|
15
|
+
// name. Its compiled files then get their decorated fields claimed, and when it ships its sources
|
|
16
|
+
// and says where they are (`rootDir`, `outDir`, as in its tsconfig), every import of a compiled file
|
|
17
|
+
// loads its source instead, which the app compiles like its own.
|
|
18
|
+
|
|
19
|
+
export interface LibraryOptions {
|
|
20
|
+
/** Where the package keeps the sources its build output comes from (tsc's `rootDir`), relative to its package.json. */
|
|
21
|
+
rootDir?: string;
|
|
22
|
+
/** Where its build output is (tsc's `outDir`). With `rootDir`, the package is compiled from its sources. */
|
|
23
|
+
outDir?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Packages compiled like the app's own code, by name (`"@scope/*"` names a whole scope): `true` or
|
|
28
|
+
* options opt a package in, `false` leaves it as published even when its package.json opts in.
|
|
29
|
+
*/
|
|
30
|
+
export type LibrariesOption = Record<string, boolean | LibraryOptions>;
|
|
31
|
+
|
|
32
|
+
/** What the app compiles in its own files, and so in the libraries' too. */
|
|
33
|
+
export interface LibraryTransforms {
|
|
34
|
+
keyedLists: boolean;
|
|
35
|
+
/** The ids the template compiler takes, or null when it is off. */
|
|
36
|
+
templates: RegExp | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PackageInfo {
|
|
40
|
+
dir: string;
|
|
41
|
+
library: boolean;
|
|
42
|
+
/** Absolute and ending in a separator, when the package is compiled from its sources. */
|
|
43
|
+
source: { rootDir: string; outDir: string } | null;
|
|
44
|
+
/** Its dependencies, peer and optional ones included. */
|
|
45
|
+
deps: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface PackageJson {
|
|
49
|
+
name?: unknown;
|
|
50
|
+
praxisjs?: unknown;
|
|
51
|
+
dependencies?: unknown;
|
|
52
|
+
devDependencies?: unknown;
|
|
53
|
+
peerDependencies?: unknown;
|
|
54
|
+
optionalDependencies?: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// What tsc compiles to each output extension.
|
|
58
|
+
const SOURCE_EXTENSIONS = new Map([
|
|
59
|
+
[".js", [".tsx", ".ts", ".jsx", ".js"]],
|
|
60
|
+
[".jsx", [".tsx", ".jsx"]],
|
|
61
|
+
[".mjs", [".mts", ".mjs"]],
|
|
62
|
+
]);
|
|
63
|
+
const JSX_FILE = /\.[jt]sx$/;
|
|
64
|
+
const JSX_PRAGMA = "/* @jsxImportSource @praxisjs/jsx */ ";
|
|
65
|
+
|
|
66
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
67
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
68
|
+
|
|
69
|
+
const keys = (value: unknown): string[] => (isRecord(value) ? Object.keys(value) : []);
|
|
70
|
+
|
|
71
|
+
const cleanId = (id: string) => id.split("?")[0];
|
|
72
|
+
|
|
73
|
+
function readJson(file: string): PackageJson | null {
|
|
74
|
+
try {
|
|
75
|
+
const json: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
76
|
+
return isRecord(json) ? json : null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `@scope/name/sub` → `@scope/name`, `name/sub` → `name`. */
|
|
83
|
+
function packageName(specifier: string): string {
|
|
84
|
+
const parts = specifier.split("/");
|
|
85
|
+
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const inside = (dir: string, path: string) => path.startsWith(dir.endsWith(sep) ? dir : dir + sep);
|
|
89
|
+
|
|
90
|
+
export class Libraries {
|
|
91
|
+
/** The package each directory belongs to, or null outside of any. */
|
|
92
|
+
private readonly packages = new Map<string, PackageInfo | null>();
|
|
93
|
+
/** Names of the libraries compiled from their sources, found by `discover`. */
|
|
94
|
+
private readonly sourceNames = new Set<string>();
|
|
95
|
+
|
|
96
|
+
constructor(readonly option: LibrariesOption = {}) {}
|
|
97
|
+
|
|
98
|
+
clear(): void {
|
|
99
|
+
this.packages.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private setting(name: string): boolean | LibraryOptions | undefined {
|
|
103
|
+
if (name in this.option) return this.option[name];
|
|
104
|
+
const scope = `${name.split("/")[0]}/*`;
|
|
105
|
+
return name.startsWith("@") && scope in this.option ? this.option[scope] : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The package whose package.json is in `dir`, or null when it has none with a name. */
|
|
109
|
+
private packageIn(dir: string): PackageInfo | null {
|
|
110
|
+
const json = readJson(join(dir, "package.json"));
|
|
111
|
+
if (json === null || typeof json.name !== "string") return null;
|
|
112
|
+
const field = isRecord(json.praxisjs) ? json.praxisjs : {};
|
|
113
|
+
const setting = this.setting(json.name);
|
|
114
|
+
const library = setting === undefined ? field.optimize === true : setting !== false;
|
|
115
|
+
const options = isRecord(setting) ? setting : field;
|
|
116
|
+
const path = (value: unknown) => {
|
|
117
|
+
if (typeof value !== "string") return null;
|
|
118
|
+
const full = resolve(dir, value);
|
|
119
|
+
return inside(dir, full) ? full + sep : null;
|
|
120
|
+
};
|
|
121
|
+
const rootDir = path(options.rootDir);
|
|
122
|
+
const outDir = path(options.outDir);
|
|
123
|
+
return {
|
|
124
|
+
dir,
|
|
125
|
+
library,
|
|
126
|
+
source: library && rootDir !== null && outDir !== null ? { rootDir, outDir } : null,
|
|
127
|
+
deps: [...new Set([...keys(json.dependencies), ...keys(json.peerDependencies), ...keys(json.optionalDependencies)])],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The package `file` belongs to. */
|
|
132
|
+
private packageOf(file: string): PackageInfo | null {
|
|
133
|
+
const visited: string[] = [];
|
|
134
|
+
let found: PackageInfo | null = null;
|
|
135
|
+
for (let dir = dirname(file); ; dir = dirname(dir)) {
|
|
136
|
+
const cached = this.packages.get(dir);
|
|
137
|
+
if (cached !== undefined) {
|
|
138
|
+
found = cached;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
visited.push(dir);
|
|
142
|
+
found = this.packageIn(dir);
|
|
143
|
+
if (found !== null || dirname(dir) === dir) break;
|
|
144
|
+
}
|
|
145
|
+
for (const dir of visited) this.packages.set(dir, found);
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether module `id` is a file of a library compiled like the app's own code. */
|
|
150
|
+
isLibraryFile(id: string): boolean {
|
|
151
|
+
return !id.startsWith("\0") && this.packageOf(cleanId(id))?.library === true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The source a compiled file of a library comes from, or null when it is not compiled from its sources. */
|
|
155
|
+
sourceFile(file: string): string | null {
|
|
156
|
+
const source = this.packageOf(file)?.source;
|
|
157
|
+
if (source == null || !inside(source.outDir, file)) return null;
|
|
158
|
+
const relative = file.slice(source.outDir.length);
|
|
159
|
+
const extension = /\.[^./\\]+$/.exec(relative)?.[0] ?? "";
|
|
160
|
+
const candidates = SOURCE_EXTENSIONS.get(extension);
|
|
161
|
+
if (candidates === undefined) return null;
|
|
162
|
+
const base = join(source.rootDir, relative.slice(0, -extension.length));
|
|
163
|
+
return candidates.map((candidate) => base + candidate).find((candidate) => existsSync(candidate)) ?? null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Whether resolving `specifier` from `importer` may land in a compiled file that has a source. */
|
|
167
|
+
mayResolveToSource(specifier: string, importer: string | undefined): boolean {
|
|
168
|
+
if (specifier.startsWith("\0")) return false;
|
|
169
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("/")) return this.sourceNames.has(packageName(specifier));
|
|
170
|
+
if (importer === undefined || importer.startsWith("\0")) return false;
|
|
171
|
+
const file = cleanId(importer);
|
|
172
|
+
const source = this.packageOf(file)?.source;
|
|
173
|
+
return source != null && inside(source.outDir, file);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The libraries the app depends on, directly or through other libraries, and those the option names. */
|
|
177
|
+
discover(root: string): string[] {
|
|
178
|
+
const found = new Set<string>();
|
|
179
|
+
const visited = new Set<string>();
|
|
180
|
+
const json = readJson(join(root, "package.json"));
|
|
181
|
+
const deps = json === null ? [] : [
|
|
182
|
+
...keys(json.dependencies), ...keys(json.devDependencies), ...keys(json.peerDependencies), ...keys(json.optionalDependencies),
|
|
183
|
+
];
|
|
184
|
+
const queue = [{ dir: root, deps }];
|
|
185
|
+
// Grows while it is walked: an array iterator reads the length at every step.
|
|
186
|
+
for (const next of queue) {
|
|
187
|
+
for (const dep of next.deps) {
|
|
188
|
+
const installed = this.installed(dep, next.dir);
|
|
189
|
+
if (installed?.library !== true) continue;
|
|
190
|
+
found.add(dep);
|
|
191
|
+
if (installed.source !== null) this.sourceNames.add(dep);
|
|
192
|
+
if (visited.has(installed.dir)) continue;
|
|
193
|
+
visited.add(installed.dir);
|
|
194
|
+
queue.push({ dir: installed.dir, deps: installed.deps });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const [name, value] of Object.entries(this.option)) {
|
|
198
|
+
if (value !== false && !name.endsWith("/*")) found.add(name);
|
|
199
|
+
}
|
|
200
|
+
return [...found];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The package `name` resolves to from `dir`, as Node looks it up. */
|
|
204
|
+
private installed(name: string, dir: string): PackageInfo | null {
|
|
205
|
+
for (let current = dir; ; current = dirname(current)) {
|
|
206
|
+
const candidate = join(current, "node_modules", name);
|
|
207
|
+
if (existsSync(join(candidate, "package.json"))) {
|
|
208
|
+
const real = realpathSync(candidate);
|
|
209
|
+
const info = this.packageIn(real);
|
|
210
|
+
this.packages.set(real, info);
|
|
211
|
+
return info;
|
|
212
|
+
}
|
|
213
|
+
if (dirname(current) === current) return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type ResolveSkippingSelf = (source: string, importer: string | undefined) => Promise<Rolldown.PartialResolvedId | null>;
|
|
219
|
+
|
|
220
|
+
// The compiled file a module resolves to, swapped for its source when the library has one.
|
|
221
|
+
async function resolveToSource(libraries: Libraries, resolveSkippingSelf: ResolveSkippingSelf, source: string, importer: string | undefined) {
|
|
222
|
+
if (!libraries.mayResolveToSource(source, importer)) return null;
|
|
223
|
+
const resolved = await resolveSkippingSelf(source, importer);
|
|
224
|
+
if (resolved === null || (resolved.external !== undefined && resolved.external !== false)) return resolved;
|
|
225
|
+
const file = cleanId(resolved.id);
|
|
226
|
+
const sourceFile = libraries.sourceFile(file);
|
|
227
|
+
return sourceFile === null ? resolved : { ...resolved, id: sourceFile + resolved.id.slice(file.length) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The JSX runtime the library is written against, pinned in a comment that leads the file: the app
|
|
231
|
+
// configures the one of its own files, and the dependency optimizer has none.
|
|
232
|
+
function pinJsxRuntime(libraries: Libraries, code: string, id: string) {
|
|
233
|
+
const file = cleanId(id);
|
|
234
|
+
if (!JSX_FILE.test(file) || code.includes("@jsxImportSource") || !libraries.isLibraryFile(id)) return null;
|
|
235
|
+
const out = new MappedOutput(code);
|
|
236
|
+
out.insert(JSX_PRAGMA, 0);
|
|
237
|
+
out.copy(0, code.length);
|
|
238
|
+
return out.result(file);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The same transforms, for Vite's dependency optimizer: it bundles the dependencies it pre-bundles
|
|
243
|
+
* before any Vite plugin sees them. Vite keys its cache of pre-bundled dependencies on the names of
|
|
244
|
+
* these plugins, so the first one carries a hash of what they do.
|
|
245
|
+
*/
|
|
246
|
+
export function prebundlePlugins(libraries: Libraries, transforms: LibraryTransforms): Rolldown.Plugin[] {
|
|
247
|
+
const isLibrary = (id: string) => libraries.isLibraryFile(id);
|
|
248
|
+
const fingerprint = createHash("sha256")
|
|
249
|
+
.update(JSON.stringify({ option: libraries.option, keyedLists: transforms.keyedLists, templates: transforms.templates?.toString() }))
|
|
250
|
+
.digest("hex")
|
|
251
|
+
.slice(0, 8);
|
|
252
|
+
const plugins: Rolldown.Plugin[] = [{
|
|
253
|
+
name: `praxisjs:libraries(${fingerprint})`,
|
|
254
|
+
resolveId(source, importer, options) {
|
|
255
|
+
return resolveToSource(libraries, (s, i) => this.resolve(s, i, { ...options, skipSelf: true }), source, importer);
|
|
256
|
+
},
|
|
257
|
+
}];
|
|
258
|
+
if (transforms.keyedLists) {
|
|
259
|
+
plugins.push({
|
|
260
|
+
name: "praxisjs:keyed-lists",
|
|
261
|
+
transform(code, id) {
|
|
262
|
+
if (!JSX_FILE.test(cleanId(id)) || !isLibrary(id)) return null;
|
|
263
|
+
const result = transformKeyedLists(code, cleanId(id));
|
|
264
|
+
return result?.map == null ? null : { code: result.code, map: result.map };
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
plugins.push({
|
|
269
|
+
name: "praxisjs:decorators",
|
|
270
|
+
resolveId(id) {
|
|
271
|
+
return id === HELPERS_ID ? `\0${HELPERS_ID}` : null;
|
|
272
|
+
},
|
|
273
|
+
load(id) {
|
|
274
|
+
return id === `\0${HELPERS_ID}` ? helpersModule() : null;
|
|
275
|
+
},
|
|
276
|
+
transform(code, id) {
|
|
277
|
+
if (!isLibrary(id)) return null;
|
|
278
|
+
return transformDecorators(code, id, isLibrary, (message) => { this.warn(message); });
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
const { templates } = transforms;
|
|
282
|
+
if (templates !== null) {
|
|
283
|
+
plugins.push({
|
|
284
|
+
name: "praxisjs:templates",
|
|
285
|
+
transform(code, id) {
|
|
286
|
+
return templates.test(id) && isLibrary(id) ? transformTemplates(code, cleanId(id)) : null;
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
plugins.push({
|
|
291
|
+
name: "praxisjs:libraries-jsx",
|
|
292
|
+
transform(code, id) {
|
|
293
|
+
return pinJsxRuntime(libraries, code, id);
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
return plugins;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Lets the other PraxisJS plugins compile the libraries that opt in: runs them inside Vite's
|
|
301
|
+
* dependency optimizer too, keeps the libraries out of SSR externals (Node would load them as
|
|
302
|
+
* published), loads their sources in place of their build output, and pins the JSX runtime their
|
|
303
|
+
* files compile against.
|
|
304
|
+
*/
|
|
305
|
+
export function librariesPlugin(libraries: Libraries, transforms: LibraryTransforms): Plugin {
|
|
306
|
+
return {
|
|
307
|
+
name: "praxisjs:libraries",
|
|
308
|
+
enforce: "pre",
|
|
309
|
+
config(config) {
|
|
310
|
+
const found = libraries.discover(resolve(config.root ?? process.cwd()));
|
|
311
|
+
if (found.length === 0) return null;
|
|
312
|
+
return {
|
|
313
|
+
optimizeDeps: { rolldownOptions: { plugins: prebundlePlugins(libraries, transforms) } },
|
|
314
|
+
ssr: { noExternal: found },
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
buildStart() {
|
|
318
|
+
libraries.clear();
|
|
319
|
+
},
|
|
320
|
+
resolveId(source, importer, options) {
|
|
321
|
+
return resolveToSource(libraries, (s, i) => this.resolve(s, i, { ...options, skipSelf: true }), source, importer);
|
|
322
|
+
},
|
|
323
|
+
transform(code, id) {
|
|
324
|
+
// After the other transforms that add imports at the top, so the pragma still leads the file.
|
|
325
|
+
return pinJsxRuntime(libraries, code, id);
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export interface SourceMapV3 {
|
|
2
|
+
version: 3;
|
|
3
|
+
file: string;
|
|
4
|
+
sources: string[];
|
|
5
|
+
sourcesContent: string[];
|
|
6
|
+
names: string[];
|
|
7
|
+
mappings: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ── source map ───────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
13
|
+
|
|
14
|
+
export function encodeVlq(value: number): string {
|
|
15
|
+
let vlq = value < 0 ? (-value << 1) | 1 : value << 1;
|
|
16
|
+
let out = "";
|
|
17
|
+
do {
|
|
18
|
+
let digit = vlq & 31;
|
|
19
|
+
vlq >>>= 5;
|
|
20
|
+
if (vlq > 0) digit |= 32;
|
|
21
|
+
out += BASE64[digit];
|
|
22
|
+
} while (vlq > 0);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class MappedOutput {
|
|
27
|
+
private code = "";
|
|
28
|
+
private readonly lines: string[][] = [[]];
|
|
29
|
+
private column = 0;
|
|
30
|
+
private prevSourceLine = 0;
|
|
31
|
+
private prevSourceColumn = 0;
|
|
32
|
+
private prevColumn = 0;
|
|
33
|
+
private readonly lineStarts: number[];
|
|
34
|
+
|
|
35
|
+
constructor(private readonly source: string) {
|
|
36
|
+
this.lineStarts = [0];
|
|
37
|
+
for (let i = 0; i < source.length; i++) {
|
|
38
|
+
if (source.charCodeAt(i) === 10) this.lineStarts.push(i + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private position(offset: number): [number, number] {
|
|
43
|
+
let lo = 0;
|
|
44
|
+
let hi = this.lineStarts.length - 1;
|
|
45
|
+
while (lo < hi) {
|
|
46
|
+
const mid = (lo + hi + 1) >> 1;
|
|
47
|
+
if (this.lineStarts[mid] <= offset) lo = mid;
|
|
48
|
+
else hi = mid - 1;
|
|
49
|
+
}
|
|
50
|
+
return [lo, offset - this.lineStarts[lo]];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private segment(offset: number): void {
|
|
54
|
+
const [line, column] = this.position(offset);
|
|
55
|
+
const current = this.lines[this.lines.length - 1];
|
|
56
|
+
current.push(
|
|
57
|
+
encodeVlq(this.column - this.prevColumn) +
|
|
58
|
+
encodeVlq(0) +
|
|
59
|
+
encodeVlq(line - this.prevSourceLine) +
|
|
60
|
+
encodeVlq(column - this.prevSourceColumn),
|
|
61
|
+
);
|
|
62
|
+
this.prevColumn = this.column;
|
|
63
|
+
this.prevSourceLine = line;
|
|
64
|
+
this.prevSourceColumn = column;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Appends text taken verbatim from the source, starting at `offset`. */
|
|
68
|
+
copy(from: number, to: number): void {
|
|
69
|
+
if (from >= to) return;
|
|
70
|
+
const text = this.source.slice(from, to);
|
|
71
|
+
this.segment(from);
|
|
72
|
+
let lineStart = 0;
|
|
73
|
+
for (let i = 0; i < text.length; i++) {
|
|
74
|
+
if (text.charCodeAt(i) !== 10) continue;
|
|
75
|
+
this.newLine();
|
|
76
|
+
lineStart = i + 1;
|
|
77
|
+
if (lineStart < text.length) this.segment(from + lineStart);
|
|
78
|
+
}
|
|
79
|
+
this.column += text.length - lineStart;
|
|
80
|
+
this.code += text;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Appends generated text, mapped to the source position `offset`. */
|
|
84
|
+
insert(text: string, offset: number): void {
|
|
85
|
+
this.segment(offset);
|
|
86
|
+
const lastBreak = text.lastIndexOf("\n");
|
|
87
|
+
if (lastBreak === -1) {
|
|
88
|
+
this.column += text.length;
|
|
89
|
+
} else {
|
|
90
|
+
for (let i = 0; i <= lastBreak; i++) if (text.charCodeAt(i) === 10) this.newLine();
|
|
91
|
+
this.column = text.length - lastBreak - 1;
|
|
92
|
+
}
|
|
93
|
+
this.code += text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private newLine(): void {
|
|
97
|
+
this.lines.push([]);
|
|
98
|
+
this.column = 0;
|
|
99
|
+
this.prevColumn = 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
result(file: string): { code: string; map: SourceMapV3 } {
|
|
103
|
+
return {
|
|
104
|
+
code: this.code,
|
|
105
|
+
map: {
|
|
106
|
+
version: 3,
|
|
107
|
+
file,
|
|
108
|
+
sources: [file],
|
|
109
|
+
sourcesContent: [this.source],
|
|
110
|
+
names: [],
|
|
111
|
+
mappings: this.lines.map((segments) => segments.join(",")).join(";"),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A map of `source` moved down by `lines` generated lines, each of its lines otherwise in place. */
|
|
118
|
+
export function shiftedLines(source: string, file: string, lines: number): SourceMapV3 {
|
|
119
|
+
const count = source.split("\n").length;
|
|
120
|
+
return {
|
|
121
|
+
version: 3,
|
|
122
|
+
file,
|
|
123
|
+
sources: [file],
|
|
124
|
+
sourcesContent: [source],
|
|
125
|
+
names: [],
|
|
126
|
+
mappings: ";".repeat(lines) + ["AAAA", ...Array<string>(count - 1).fill("AACA")].join(";"),
|
|
127
|
+
};
|
|
128
|
+
}
|