@litsx/compiler 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/src/index.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ export type TransformLitsxOptions = {
2
+ filename?: string;
3
+ parserPlugins?: string[];
4
+ sourceMaps?: boolean;
5
+ jsxTemplate?: boolean;
6
+ jsxTemplateOptions?: object;
7
+ authoringPlugins?: unknown[];
8
+ outputPlugins?: unknown[];
9
+ requireJsx?: boolean;
10
+ };
11
+
12
+ export type LitsxCompilationSession = {
13
+ transform(source: string, options?: TransformLitsxOptions): Promise<TransformLitsxResult>;
14
+ transformSync(source: string, options?: TransformLitsxOptions): TransformLitsxResult;
15
+ getTypecheckSession(rawArgs?: string[]): unknown;
16
+ invalidate(files?: string[] | null): void;
17
+ dispose(): void;
18
+ };
19
+
20
+ export type TransformLitsxResult = {
21
+ code: string;
22
+ map: object | null;
23
+ metadata: Record<string, unknown>;
24
+ };
25
+
26
+ export type PreparedLitsxAuthoredInput = {
27
+ filename?: string;
28
+ virtualization: {
29
+ code?: string;
30
+ map?: object | null;
31
+ } | null;
32
+ inputAst: object;
33
+ authoredWarnings: unknown[];
34
+ };
35
+
36
+ export function ensureLitsxParserPlugins(
37
+ filename?: string,
38
+ parserPlugins?: string[],
39
+ options?: { requireJsx?: boolean }
40
+ ): string[];
41
+
42
+ export function prepareLitsxAuthoredInput(
43
+ source: string,
44
+ options?: TransformLitsxOptions,
45
+ runtime?: {
46
+ parse: (...args: unknown[]) => object;
47
+ transformFromAstSync: (...args: unknown[]) => { ast?: object } | null | undefined;
48
+ }
49
+ ): PreparedLitsxAuthoredInput;
50
+
51
+ export function transformLitsx(
52
+ source: string,
53
+ options?: TransformLitsxOptions
54
+ ): Promise<TransformLitsxResult>;
55
+
56
+ export function transformLitsxSync(
57
+ source: string,
58
+ options?: TransformLitsxOptions
59
+ ): TransformLitsxResult;
60
+
61
+ export function createLitsxCompilationSession(options?: {
62
+ projectPath?: string;
63
+ transformOptions?: TransformLitsxOptions;
64
+ }): LitsxCompilationSession;
package/src/index.js ADDED
@@ -0,0 +1,438 @@
1
+ import babelCore from "@babel/core";
2
+ import {
3
+ createLitsxPresetPlugins,
4
+ detectLitsxSourceFeatures,
5
+ } from "@litsx/babel-preset-litsx";
6
+ import { ensureTypescriptModule } from "@litsx/babel-preset-litsx/internal/transform-litsx-properties";
7
+ import { createLitsxTypecheckSession } from "@litsx/typescript-plugin/typecheck";
8
+ import {
9
+ createStandaloneTsSession,
10
+ normalizeFilePath,
11
+ } from "@litsx/typescript-session";
12
+ import {
13
+ patchLitAttributeSourcemap,
14
+ } from "@litsx/babel-plugin-transform-jsx-html-template";
15
+ import {
16
+ ensureLitsxParserPlugins,
17
+ prepareLitsxAuthoredInput,
18
+ } from "./authored-input.js";
19
+ import { mergeLitsxWarnings } from "./warnings.js";
20
+ export {
21
+ ensureLitsxParserPlugins,
22
+ prepareLitsxAuthoredInput,
23
+ } from "./authored-input.js";
24
+
25
+ const { transformFromAstAsync, transformFromAstSync } = babelCore;
26
+ const PROFILE_ENABLED = process.env.LITSX_PROFILE === "1";
27
+ const PRESET_PLUGIN_CACHE = new WeakMap();
28
+ const DEFAULT_PRESET_PLUGIN_CACHE = new Map();
29
+
30
+ function createStandaloneTsCompilerOptions(ts) {
31
+ return {
32
+ target: ts.ScriptTarget.ESNext,
33
+ module: ts.ModuleKind.ESNext,
34
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
35
+ jsx: ts.JsxEmit.Preserve,
36
+ allowJs: true,
37
+ checkJs: false,
38
+ skipLibCheck: true,
39
+ strict: false,
40
+ esModuleInterop: true,
41
+ allowSyntheticDefaultImports: true,
42
+ types: [],
43
+ };
44
+ }
45
+
46
+ function getSourceFeaturesCacheKey(sourceFeatures) {
47
+ if (!sourceFeatures) {
48
+ return "all";
49
+ }
50
+
51
+ return [
52
+ sourceFeatures.hooks ? "1" : "0",
53
+ sourceFeatures.domRefs ? "1" : "0",
54
+ sourceFeatures.scopedElements ? "1" : "0",
55
+ ].join("");
56
+ }
57
+
58
+ function profilePhase(name, callback, profile = null) {
59
+ if (!PROFILE_ENABLED) {
60
+ return callback();
61
+ }
62
+
63
+ const start = performance.now();
64
+ try {
65
+ return callback();
66
+ } finally {
67
+ const durationMs = performance.now() - start;
68
+ if (profile) {
69
+ profile.push({ name, durationMs });
70
+ }
71
+ if (PROFILE_ENABLED) {
72
+ globalThis.__litsxProfileEvents ??= [];
73
+ globalThis.__litsxProfileEvents.push({
74
+ namespace: "compiler",
75
+ name,
76
+ durationMs,
77
+ });
78
+ }
79
+ }
80
+ }
81
+
82
+ function normalizePluginList(plugins) {
83
+ return Array.isArray(plugins) ? plugins : [];
84
+ }
85
+
86
+ function getStandaloneTsSessionKey(filename = "", ts = ensureTypescriptModule()) {
87
+ const normalizedFilename = normalizeFilePath(filename);
88
+ const directory = normalizedFilename ? normalizedFilename.slice(0, normalizedFilename.lastIndexOf("/")) || "/" : "/";
89
+ return JSON.stringify({
90
+ directory,
91
+ compilerOptions: createStandaloneTsCompilerOptions(ts),
92
+ });
93
+ }
94
+
95
+ function getMemoizedPresetPlugins(options, sourceFeatures = null, session = null) {
96
+ const featureKey = getSourceFeaturesCacheKey(sourceFeatures);
97
+ if (session) {
98
+ const cache = session.presetPluginsByOptions;
99
+ const optionsKey = options && typeof options === "object" ? options : null;
100
+
101
+ if (!optionsKey) {
102
+ if (!cache.default.has(featureKey)) {
103
+ cache.default.set(featureKey, createLitsxPresetPlugins({}, sourceFeatures));
104
+ }
105
+ return cache.default.get(featureKey);
106
+ }
107
+
108
+ let cachedPluginsByFeature = cache.byOptions.get(optionsKey);
109
+ if (!cachedPluginsByFeature) {
110
+ cachedPluginsByFeature = new Map();
111
+ cache.byOptions.set(optionsKey, cachedPluginsByFeature);
112
+ }
113
+
114
+ const cachedPlugins = cachedPluginsByFeature.get(featureKey);
115
+ if (cachedPlugins) {
116
+ return cachedPlugins;
117
+ }
118
+
119
+ const plugins = createLitsxPresetPlugins(options, sourceFeatures);
120
+ cachedPluginsByFeature.set(featureKey, plugins);
121
+ return plugins;
122
+ }
123
+
124
+ if (!options || typeof options !== "object") {
125
+ if (!DEFAULT_PRESET_PLUGIN_CACHE.has(featureKey)) {
126
+ DEFAULT_PRESET_PLUGIN_CACHE.set(
127
+ featureKey,
128
+ createLitsxPresetPlugins({}, sourceFeatures),
129
+ );
130
+ }
131
+ return DEFAULT_PRESET_PLUGIN_CACHE.get(featureKey);
132
+ }
133
+
134
+ let cachedPluginsByFeature = PRESET_PLUGIN_CACHE.get(options);
135
+ if (!cachedPluginsByFeature) {
136
+ cachedPluginsByFeature = new Map();
137
+ PRESET_PLUGIN_CACHE.set(options, cachedPluginsByFeature);
138
+ }
139
+
140
+ const cachedPlugins = cachedPluginsByFeature.get(featureKey);
141
+ if (cachedPlugins) {
142
+ return cachedPlugins;
143
+ }
144
+
145
+ const plugins = createLitsxPresetPlugins(options, sourceFeatures);
146
+ cachedPluginsByFeature.set(featureKey, plugins);
147
+ return plugins;
148
+ }
149
+
150
+ function getSessionFeatureCacheKey(source, options = {}) {
151
+ return `${options.filename || ""}:${source}`;
152
+ }
153
+
154
+ function createCompilerCaches() {
155
+ return {
156
+ sourceFeatures: new Map(),
157
+ authoredInput: new Map(),
158
+ presetPluginsByOptions: {
159
+ default: new Map(),
160
+ byOptions: new WeakMap(),
161
+ },
162
+ };
163
+ }
164
+
165
+ function createStandaloneCompilerTsSession(options = {}) {
166
+ const typescriptModule = options.typescriptModule || ensureTypescriptModule();
167
+ return createStandaloneTsSession({
168
+ sessionKey: getStandaloneTsSessionKey(options.filename, typescriptModule),
169
+ typescript: typescriptModule,
170
+ compilerOptions: createStandaloneTsCompilerOptions(typescriptModule),
171
+ });
172
+ }
173
+
174
+ export function createLitsxCompilationSession(sessionOptions = {}) {
175
+ const caches = createCompilerCaches();
176
+ const session = {
177
+ projectPath: sessionOptions.projectPath || null,
178
+ transformOptions: sessionOptions.transformOptions || {},
179
+ typescriptSession:
180
+ sessionOptions.projectPath
181
+ ? createLitsxTypecheckSession(["--project", sessionOptions.projectPath]).projectSession
182
+ : createStandaloneCompilerTsSession({
183
+ filename: sessionOptions.transformOptions?.filename,
184
+ typescriptModule: sessionOptions.typescriptModule,
185
+ }),
186
+ presetPluginsByOptions: caches.presetPluginsByOptions,
187
+ sourceFeaturesCache: caches.sourceFeatures,
188
+ authoredInputCache: caches.authoredInput,
189
+ transform(source, options = {}) {
190
+ return transformLitsx(source, {
191
+ ...this.transformOptions,
192
+ ...options,
193
+ typescriptSession: this.typescriptSession,
194
+ __litsxCompilationSession: this,
195
+ });
196
+ },
197
+ transformSync(source, options = {}) {
198
+ return transformLitsxSync(source, {
199
+ ...this.transformOptions,
200
+ ...options,
201
+ typescriptSession: this.typescriptSession,
202
+ __litsxCompilationSession: this,
203
+ });
204
+ },
205
+ getTypecheckSession(rawArgs = this.projectPath ? ["--project", this.projectPath] : []) {
206
+ return createLitsxTypecheckSession(rawArgs, {
207
+ projectSession: this.typescriptSession,
208
+ });
209
+ },
210
+ invalidate(files = null) {
211
+ if (!files || files.length === 0) {
212
+ this.sourceFeaturesCache.clear();
213
+ this.authoredInputCache.clear();
214
+ this.typescriptSession?.invalidate?.({ host: true });
215
+ return;
216
+ }
217
+
218
+ for (const file of files) {
219
+ const normalizedFile = normalizeFilePath(file);
220
+ for (const key of [...this.sourceFeaturesCache.keys()]) {
221
+ if (key.startsWith(`${normalizedFile}:`)) {
222
+ this.sourceFeaturesCache.delete(key);
223
+ }
224
+ }
225
+ for (const key of [...this.authoredInputCache.keys()]) {
226
+ if (key.startsWith(`${normalizedFile}:`)) {
227
+ this.authoredInputCache.delete(key);
228
+ }
229
+ }
230
+ if (/\.(jsx|tsx|js|ts|litsx)$/.test(file) || file.endsWith(".litsx.jsx")) {
231
+ this.typescriptSession?.invalidate?.();
232
+ }
233
+ }
234
+ },
235
+ dispose() {
236
+ this.invalidate();
237
+ this.typescriptSession?.clearOverlayFiles?.();
238
+ this.typescriptSession = null;
239
+ },
240
+ };
241
+ return session;
242
+ }
243
+
244
+ export function createLitsxTransformConfig(source, options = {}) {
245
+ const profile = PROFILE_ENABLED ? [] : null;
246
+ const compilationSession = options.__litsxCompilationSession || null;
247
+ const memoizationOptions = options.__litsxMemoizeOptions || options;
248
+ const featureCacheKey = getSessionFeatureCacheKey(source, options);
249
+ const sourceFeatures = profilePhase(
250
+ "feature-detection",
251
+ () => {
252
+ if (compilationSession?.sourceFeaturesCache?.has(featureCacheKey)) {
253
+ return compilationSession.sourceFeaturesCache.get(featureCacheKey);
254
+ }
255
+ const detected = detectLitsxSourceFeatures(source, options);
256
+ compilationSession?.sourceFeaturesCache?.set(featureCacheKey, detected);
257
+ return detected;
258
+ },
259
+ profile,
260
+ );
261
+ const authoredInputCacheKey = featureCacheKey;
262
+ const { filename, virtualization, inputAst, authoredWarnings } = profilePhase(
263
+ "authored-input",
264
+ () => {
265
+ if (compilationSession?.authoredInputCache?.has(authoredInputCacheKey)) {
266
+ return compilationSession.authoredInputCache.get(authoredInputCacheKey);
267
+ }
268
+ const prepared = prepareLitsxAuthoredInput(
269
+ source,
270
+ options,
271
+ {
272
+ transformFromAstSync,
273
+ }
274
+ );
275
+ compilationSession?.authoredInputCache?.set(authoredInputCacheKey, prepared);
276
+ return prepared;
277
+ },
278
+ profile,
279
+ );
280
+ const outputPlugins = normalizePluginList(options.outputPlugins);
281
+ const presetPlugins = profilePhase(
282
+ "preset-plugins",
283
+ () => getMemoizedPresetPlugins(memoizationOptions, sourceFeatures, compilationSession),
284
+ profile,
285
+ );
286
+
287
+ return {
288
+ filename,
289
+ inputAst,
290
+ authoredWarnings,
291
+ profile,
292
+ babelOptions: {
293
+ filename,
294
+ sourceFileName: filename,
295
+ configFile: false,
296
+ babelrc: false,
297
+ inputSourceMap:
298
+ options.sourceMaps === true ? virtualization?.map ?? undefined : undefined,
299
+ sourceMaps: options.sourceMaps === true,
300
+ plugins: [
301
+ ...presetPlugins,
302
+ ...outputPlugins,
303
+ ],
304
+ },
305
+ };
306
+ }
307
+
308
+ function finalizeTransformResult(result, options, authoredWarnings = [], profile = []) {
309
+ if (!result) {
310
+ return {
311
+ code: "",
312
+ map: null,
313
+ metadata: profile?.length > 0 ? { litsxProfile: profile } : {},
314
+ };
315
+ }
316
+
317
+ const metadata = {
318
+ ...(result.metadata || {}),
319
+ };
320
+ const mergedWarnings = mergeLitsxWarnings(
321
+ metadata.litsxWarnings || [],
322
+ authoredWarnings,
323
+ { filename: options.filename }
324
+ );
325
+ if (mergedWarnings.length > 0) {
326
+ metadata.litsxWarnings = mergedWarnings;
327
+ }
328
+ if (profile?.length > 0) {
329
+ metadata.litsxProfile = profile;
330
+ }
331
+ const templateAttributeMappings = metadata.litsxTemplateAttributeMappings || [];
332
+ const map =
333
+ options.sourceMaps === true
334
+ ? options.jsxTemplate === false
335
+ ? result.map ?? null
336
+ : templateAttributeMappings.length === 0
337
+ ? result.map ?? null
338
+ : profilePhase(
339
+ "sourcemap-patching",
340
+ () => patchLitAttributeSourcemap(
341
+ result.code || "",
342
+ result.map ?? null,
343
+ templateAttributeMappings,
344
+ ),
345
+ profile,
346
+ )
347
+ : null;
348
+
349
+ return {
350
+ code: result.code || "",
351
+ map,
352
+ metadata,
353
+ };
354
+ }
355
+
356
+ export async function transformLitsx(source, options = {}) {
357
+ if (!options.__litsxCompilationSession) {
358
+ const standaloneTsSession = createStandaloneCompilerTsSession({
359
+ filename: options.filename,
360
+ });
361
+ const nextOptions = {
362
+ ...options,
363
+ typescriptSession: standaloneTsSession,
364
+ __litsxMemoizeOptions: options,
365
+ };
366
+ const {
367
+ inputAst,
368
+ babelOptions,
369
+ authoredWarnings,
370
+ profile,
371
+ } = createLitsxTransformConfig(source, nextOptions);
372
+ const result = await profilePhase(
373
+ "babel-transform",
374
+ () => transformFromAstAsync(inputAst, source, {
375
+ ...babelOptions,
376
+ plugins: babelOptions.plugins,
377
+ }),
378
+ profile,
379
+ );
380
+ return finalizeTransformResult(result, nextOptions, authoredWarnings, profile);
381
+ }
382
+
383
+ const {
384
+ inputAst,
385
+ babelOptions,
386
+ authoredWarnings,
387
+ profile,
388
+ } = createLitsxTransformConfig(source, options);
389
+ const result = await profilePhase(
390
+ "babel-transform",
391
+ () => transformFromAstAsync(inputAst, source, {
392
+ ...babelOptions,
393
+ plugins: babelOptions.plugins,
394
+ }),
395
+ profile,
396
+ );
397
+ return finalizeTransformResult(result, options, authoredWarnings, profile);
398
+ }
399
+
400
+ export function transformLitsxSync(source, options = {}) {
401
+ if (!options.__litsxCompilationSession) {
402
+ const standaloneTsSession = createStandaloneCompilerTsSession({
403
+ filename: options.filename,
404
+ });
405
+ const nextOptions = {
406
+ ...options,
407
+ typescriptSession: standaloneTsSession,
408
+ __litsxMemoizeOptions: options,
409
+ };
410
+ const {
411
+ inputAst,
412
+ babelOptions,
413
+ authoredWarnings,
414
+ profile,
415
+ } = createLitsxTransformConfig(source, nextOptions);
416
+ const result = profilePhase(
417
+ "babel-transform",
418
+ () => transformFromAstSync(inputAst, source, babelOptions),
419
+ profile,
420
+ );
421
+ return finalizeTransformResult(result, nextOptions, authoredWarnings, profile);
422
+ }
423
+
424
+ const {
425
+ inputAst,
426
+ babelOptions,
427
+ authoredWarnings,
428
+ profile,
429
+ } = createLitsxTransformConfig(source, options);
430
+ const result = profilePhase(
431
+ "babel-transform",
432
+ () => transformFromAstSync(inputAst, source, babelOptions),
433
+ profile,
434
+ );
435
+ return finalizeTransformResult(result, options, authoredWarnings, profile);
436
+ }
437
+
438
+ export default transformLitsx;
@@ -0,0 +1,66 @@
1
+ function normalizeLocationNumber(value) {
2
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3
+ }
4
+
5
+ export function normalizeLitsxWarning(warning, context = {}) {
6
+ const normalized = warning && typeof warning === "object" ? { ...warning } : {};
7
+
8
+ normalized.code =
9
+ (typeof normalized.code === "string" && normalized.code !== "") ||
10
+ (typeof normalized.code === "number" && Number.isFinite(normalized.code))
11
+ ? normalized.code
12
+ : null;
13
+ normalized.message =
14
+ typeof normalized.message === "string" && normalized.message !== ""
15
+ ? normalized.message
16
+ : "LitSX emitted a warning during compilation.";
17
+ normalized.filename =
18
+ typeof normalized.filename === "string" && normalized.filename !== ""
19
+ ? normalized.filename
20
+ : typeof context.filename === "string" && context.filename !== ""
21
+ ? context.filename
22
+ : null;
23
+ normalized.line = normalizeLocationNumber(normalized.line);
24
+ normalized.column = normalizeLocationNumber(normalized.column);
25
+ normalized.attributeName =
26
+ typeof normalized.attributeName === "string" && normalized.attributeName !== ""
27
+ ? normalized.attributeName
28
+ : null;
29
+ normalized.tagName =
30
+ typeof normalized.tagName === "string" && normalized.tagName !== ""
31
+ ? normalized.tagName
32
+ : null;
33
+ normalized.propName =
34
+ typeof normalized.propName === "string" && normalized.propName !== ""
35
+ ? normalized.propName
36
+ : null;
37
+
38
+ return normalized;
39
+ }
40
+
41
+ export function mergeLitsxWarnings(existingWarnings = [], additionalWarnings = [], context = {}) {
42
+ const merged = [];
43
+ const seen = new Set();
44
+
45
+ for (const rawWarning of [...existingWarnings, ...additionalWarnings]) {
46
+ const warning = normalizeLitsxWarning(rawWarning, context);
47
+ const key = [
48
+ warning.code ?? "",
49
+ warning.attributeName ?? "",
50
+ warning.tagName ?? "",
51
+ warning.propName ?? "",
52
+ warning.line ?? "",
53
+ warning.column ?? "",
54
+ warning.message ?? "",
55
+ ].join(":");
56
+
57
+ if (seen.has(key)) {
58
+ continue;
59
+ }
60
+
61
+ seen.add(key);
62
+ merged.push(warning);
63
+ }
64
+
65
+ return merged;
66
+ }