@octanejs/mdx 0.1.3 → 0.1.7
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/README.md +11 -1
- package/package.json +5 -5
- package/src/compile.js +421 -8
- package/src/octane-compiler.d.ts +26 -2
- package/src/vite.js +48 -4
- package/types/compile.d.ts +23 -0
- package/types/vite.d.ts +16 -4
package/README.md
CHANGED
|
@@ -30,6 +30,14 @@ export default defineConfig({
|
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Pass `profile: true` to both plugins for a browser profiling build:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
plugins: [octaneMdx({ profile: true }), octane({ profile: true })];
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Profiling metadata is never emitted by the MDX server transform.
|
|
40
|
+
|
|
33
41
|
`octaneMdx()` claims `.mdx`/`.md` and produces final JS, so it composes with the
|
|
34
42
|
octane plugin (which claims `.tsrx`/`.tsx`/`.ts`/`.js`) without ordering
|
|
35
43
|
hazards. SSR target selection matches the octane plugin: per-module
|
|
@@ -80,7 +88,9 @@ the special `wrapper` key is the document layout.
|
|
|
80
88
|
SSR passes call this during `renderToString`). Same observable mapping.
|
|
81
89
|
- `@octanejs/mdx/compile` — `compileMdx` / `compileMdxSync` /
|
|
82
90
|
`defaultRemarkPlugins`, the plugin's pipeline as a library (used by the SSR
|
|
83
|
-
tests, usable for static-site tooling).
|
|
91
|
+
tests, usable for static-site tooling). Results include nonfatal compiler
|
|
92
|
+
`diagnostics`; JSX warning ranges are mapped back to the authored `.mdx`
|
|
93
|
+
document. The Vite plugin publishes the same warnings during transforms.
|
|
84
94
|
|
|
85
95
|
## SSR + hydration
|
|
86
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/mdx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@jridgewell/remapping": "^2.3.5",
|
|
51
|
+
"@jridgewell/trace-mapping": "^0.3.31",
|
|
51
52
|
"@mdx-js/mdx": "^3.1.1",
|
|
52
53
|
"remark-frontmatter": "^5.0.0",
|
|
53
54
|
"remark-gfm": "^4.0.1",
|
|
@@ -55,15 +56,14 @@
|
|
|
55
56
|
"source-map": "^0.7.6"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
|
-
"octane": "0.1.
|
|
59
|
+
"octane": "0.1.10"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
|
-
"@jridgewell/trace-mapping": "^0.3.31",
|
|
62
62
|
"@shikijs/rehype": "^3.0.0",
|
|
63
63
|
"vite": "^8.0.16",
|
|
64
64
|
"vitest": "^4.1.9",
|
|
65
|
-
"@octanejs/testing-library": "0.1.
|
|
66
|
-
"octane": "0.1.
|
|
65
|
+
"@octanejs/testing-library": "0.1.7",
|
|
66
|
+
"octane": "0.1.10"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"test": "vitest run"
|
package/src/compile.js
CHANGED
|
@@ -39,8 +39,14 @@
|
|
|
39
39
|
* consuming app's `vite.config.ts` pulls it in through Node's ESM loader.
|
|
40
40
|
*/
|
|
41
41
|
import remapping from '@jridgewell/remapping';
|
|
42
|
+
import {
|
|
43
|
+
TraceMap,
|
|
44
|
+
originalPositionFor,
|
|
45
|
+
GREATEST_LOWER_BOUND,
|
|
46
|
+
LEAST_UPPER_BOUND,
|
|
47
|
+
} from '@jridgewell/trace-mapping';
|
|
42
48
|
import { compile as mdxCompile, compileSync as mdxCompileSync } from '@mdx-js/mdx';
|
|
43
|
-
import { compile as octaneCompile } from 'octane/compiler';
|
|
49
|
+
import { __analyzeNativeChangeDiagnostics, compile as octaneCompile } from 'octane/compiler';
|
|
44
50
|
import remarkFrontmatter from 'remark-frontmatter';
|
|
45
51
|
import remarkGfm from 'remark-gfm';
|
|
46
52
|
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
|
|
@@ -50,6 +56,36 @@ import { SourceMapGenerator } from 'source-map';
|
|
|
50
56
|
* @typedef {object} CompileMdxResult
|
|
51
57
|
* @property {string} code
|
|
52
58
|
* @property {unknown} map
|
|
59
|
+
* @property {Array<{
|
|
60
|
+
* code: string,
|
|
61
|
+
* severity: 'warning',
|
|
62
|
+
* message: string,
|
|
63
|
+
* filename: string,
|
|
64
|
+
* start: { offset: number, line: number, column: number },
|
|
65
|
+
* end: { offset: number, line: number, column: number },
|
|
66
|
+
* suggestions: Array<{
|
|
67
|
+
* start: { offset: number, line: number, column: number },
|
|
68
|
+
* end: { offset: number, line: number, column: number },
|
|
69
|
+
* attribute: 'onInput' | 'onInputCapture',
|
|
70
|
+
* }>,
|
|
71
|
+
* }>} diagnostics
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {object} AuthoredJsxAttributeLocation
|
|
76
|
+
* @property {string} name
|
|
77
|
+
* @property {number} start
|
|
78
|
+
* @property {number} end
|
|
79
|
+
* @property {{ start: { line: number, column: number }, end: { line: number, column: number } }} loc
|
|
80
|
+
* @property {boolean} staticallyWarned
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {object} DiagnosticMappingContext
|
|
85
|
+
* @property {AuthoredJsxAttributeLocation[]} authoredAttributes
|
|
86
|
+
* @property {WeakMap<object, AuthoredJsxAttributeLocation>} attributeLocations
|
|
87
|
+
* @property {string} source
|
|
88
|
+
* @property {string} id
|
|
53
89
|
*/
|
|
54
90
|
|
|
55
91
|
/**
|
|
@@ -57,6 +93,7 @@ import { SourceMapGenerator } from 'source-map';
|
|
|
57
93
|
* @property {'client' | 'server'} [mode] octane codegen target: `'client'` (DOM) or `'server'` (SSR HTML strings). Default `'client'`.
|
|
58
94
|
* @property {boolean} [hmr] octane compiler HMR wrapping (client only; the vite plugin wires this to serve mode).
|
|
59
95
|
* @property {boolean} [dev] octane compiler dev metadata (client only; same gate as `hmr`).
|
|
96
|
+
* @property {boolean} [profile] octane compiler profiling metadata (client only).
|
|
60
97
|
* @property {string | null} [providerImportSource]
|
|
61
98
|
* Module the emitted document reads the provider mapping from
|
|
62
99
|
* (`useMDXComponents`). Defaults per mode — `'@octanejs/mdx'` (client) /
|
|
@@ -88,8 +125,17 @@ export const defaultRemarkPlugins = [remarkGfm, remarkFrontmatter, remarkMdxFron
|
|
|
88
125
|
* @returns {Promise<CompileMdxResult>}
|
|
89
126
|
*/
|
|
90
127
|
export async function compileMdx(source, id, options = {}) {
|
|
91
|
-
const
|
|
92
|
-
|
|
128
|
+
const diagnosticContext = {
|
|
129
|
+
authoredAttributes: [],
|
|
130
|
+
attributeLocations: new WeakMap(),
|
|
131
|
+
source,
|
|
132
|
+
id,
|
|
133
|
+
};
|
|
134
|
+
const out = await mdxCompile(
|
|
135
|
+
{ value: source, path: id },
|
|
136
|
+
buildMdxOptions(id, options, diagnosticContext),
|
|
137
|
+
);
|
|
138
|
+
return octaneStage(String(out.value), out.map, source, id, options, diagnosticContext);
|
|
93
139
|
}
|
|
94
140
|
|
|
95
141
|
/**
|
|
@@ -101,16 +147,26 @@ export async function compileMdx(source, id, options = {}) {
|
|
|
101
147
|
* @returns {CompileMdxResult}
|
|
102
148
|
*/
|
|
103
149
|
export function compileMdxSync(source, id, options = {}) {
|
|
104
|
-
const
|
|
105
|
-
|
|
150
|
+
const diagnosticContext = {
|
|
151
|
+
authoredAttributes: [],
|
|
152
|
+
attributeLocations: new WeakMap(),
|
|
153
|
+
source,
|
|
154
|
+
id,
|
|
155
|
+
};
|
|
156
|
+
const out = mdxCompileSync(
|
|
157
|
+
{ value: source, path: id },
|
|
158
|
+
buildMdxOptions(id, options, diagnosticContext),
|
|
159
|
+
);
|
|
160
|
+
return octaneStage(String(out.value), out.map, source, id, options, diagnosticContext);
|
|
106
161
|
}
|
|
107
162
|
|
|
108
163
|
/**
|
|
109
164
|
* @param {string} id
|
|
110
165
|
* @param {CompileMdxOptions} options
|
|
166
|
+
* @param {DiagnosticMappingContext} diagnosticContext
|
|
111
167
|
* @returns {import('@mdx-js/mdx').CompileOptions}
|
|
112
168
|
*/
|
|
113
|
-
function buildMdxOptions(id, options) {
|
|
169
|
+
function buildMdxOptions(id, options, diagnosticContext) {
|
|
114
170
|
const format =
|
|
115
171
|
options.format && options.format !== 'detect'
|
|
116
172
|
? options.format
|
|
@@ -135,23 +191,186 @@ function buildMdxOptions(id, options) {
|
|
|
135
191
|
...(provider === null ? {} : { providerImportSource: provider }),
|
|
136
192
|
remarkPlugins: options.remarkPlugins ?? defaultRemarkPlugins,
|
|
137
193
|
rehypePlugins: options.rehypePlugins,
|
|
138
|
-
recmaPlugins: [
|
|
194
|
+
recmaPlugins: [
|
|
195
|
+
seedAuthoredJsxAttributeLocations(diagnosticContext),
|
|
196
|
+
...(options.recmaPlugins ?? []),
|
|
197
|
+
restoreAuthoredJsxAttributeLocations(diagnosticContext),
|
|
198
|
+
recmaOctaneAdapter,
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {unknown} tree
|
|
205
|
+
* @param {(attribute: Record<string, any>) => void} callback
|
|
206
|
+
*/
|
|
207
|
+
function visitJsxAttributes(tree, callback) {
|
|
208
|
+
const activeObjects = new WeakSet();
|
|
209
|
+
/** @param {unknown} value */
|
|
210
|
+
function visit(value) {
|
|
211
|
+
if (value === null || typeof value !== 'object' || activeObjects.has(value)) return;
|
|
212
|
+
activeObjects.add(value);
|
|
213
|
+
const node = /** @type {Record<string, any>} */ (value);
|
|
214
|
+
if (node.type === 'JSXAttribute') callback(node);
|
|
215
|
+
for (const [key, child] of Object.entries(node)) {
|
|
216
|
+
if (key === 'loc' || key === 'range' || key === 'position') continue;
|
|
217
|
+
if (Array.isArray(child)) child.forEach(visit);
|
|
218
|
+
else visit(child);
|
|
219
|
+
}
|
|
220
|
+
activeObjects.delete(value);
|
|
221
|
+
}
|
|
222
|
+
visit(tree);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {Record<string, any>} node
|
|
227
|
+
* @param {AuthoredJsxAttributeLocation} location
|
|
228
|
+
*/
|
|
229
|
+
function applyNodeLocation(node, location) {
|
|
230
|
+
node.start = location.start;
|
|
231
|
+
node.end = location.end;
|
|
232
|
+
node.range = [location.start, location.end];
|
|
233
|
+
node.loc = {
|
|
234
|
+
start: { ...location.loc.start },
|
|
235
|
+
end: { ...location.loc.end },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Add the exact source location missing from MDX's JSXIdentifier attribute
|
|
241
|
+
* names. Remark and rehype transforms have already established stage-one
|
|
242
|
+
* source provenance here; the parent JSXAttribute carries that location, and
|
|
243
|
+
* seeding the child makes the map point directly at the token the Octane
|
|
244
|
+
* diagnostic covers. Analyze this pre-recma tree once as well: a later
|
|
245
|
+
* transform may preserve or copy source locations, but only attributes that
|
|
246
|
+
* were already statically warned at this boundary may receive an authored fix.
|
|
247
|
+
*
|
|
248
|
+
* @param {DiagnosticMappingContext} context
|
|
249
|
+
*/
|
|
250
|
+
function seedAuthoredJsxAttributeLocations(context) {
|
|
251
|
+
return function recmaSeedAuthoredJsxAttributeLocations() {
|
|
252
|
+
/** @param {unknown} tree */
|
|
253
|
+
return (tree) => {
|
|
254
|
+
context.authoredAttributes.length = 0;
|
|
255
|
+
const occurrences = [];
|
|
256
|
+
visitJsxAttributes(tree, (attribute) => occurrences.push(attribute));
|
|
257
|
+
const occurrenceCounts = new Map();
|
|
258
|
+
for (const attribute of occurrences) {
|
|
259
|
+
occurrenceCounts.set(attribute, (occurrenceCounts.get(attribute) ?? 0) + 1);
|
|
260
|
+
}
|
|
261
|
+
for (const attribute of occurrences) {
|
|
262
|
+
if (occurrenceCounts.get(attribute) !== 1) continue;
|
|
263
|
+
const name = attribute.name;
|
|
264
|
+
if (
|
|
265
|
+
name?.type !== 'JSXIdentifier' ||
|
|
266
|
+
typeof name.name !== 'string' ||
|
|
267
|
+
typeof attribute.start !== 'number' ||
|
|
268
|
+
typeof attribute.loc?.start?.line !== 'number' ||
|
|
269
|
+
typeof attribute.loc?.start?.column !== 'number'
|
|
270
|
+
)
|
|
271
|
+
continue;
|
|
272
|
+
const location = {
|
|
273
|
+
name: name.name,
|
|
274
|
+
start: attribute.start,
|
|
275
|
+
end: attribute.start + name.name.length,
|
|
276
|
+
loc: {
|
|
277
|
+
start: {
|
|
278
|
+
line: attribute.loc.start.line,
|
|
279
|
+
column: attribute.loc.start.column,
|
|
280
|
+
},
|
|
281
|
+
end: {
|
|
282
|
+
line: attribute.loc.start.line,
|
|
283
|
+
column: attribute.loc.start.column + name.name.length,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
staticallyWarned: false,
|
|
287
|
+
};
|
|
288
|
+
context.authoredAttributes.push(location);
|
|
289
|
+
context.attributeLocations.set(attribute, location);
|
|
290
|
+
const seededName = { ...name };
|
|
291
|
+
attribute.name = seededName;
|
|
292
|
+
applyNodeLocation(seededName, location);
|
|
293
|
+
}
|
|
294
|
+
const warningRanges = new Set(
|
|
295
|
+
__analyzeNativeChangeDiagnostics(tree, context.source, context.id).diagnostics.flatMap(
|
|
296
|
+
(diagnostic) =>
|
|
297
|
+
diagnostic.suggestions.map(
|
|
298
|
+
(suggestion) => `${suggestion.start.offset}:${suggestion.end.offset}`,
|
|
299
|
+
),
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
for (const location of context.authoredAttributes) {
|
|
303
|
+
location.staticallyWarned = warningRanges.has(`${location.start}:${location.end}`);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Restore exact locations on unchanged attribute identities after user recma
|
|
311
|
+
* transforms. Classification was frozen before those transforms, so even a
|
|
312
|
+
* later plugin that copies a location cannot turn a previously-safe authored
|
|
313
|
+
* callback into an actionable replacement.
|
|
314
|
+
*
|
|
315
|
+
* @param {DiagnosticMappingContext} context
|
|
316
|
+
*/
|
|
317
|
+
function restoreAuthoredJsxAttributeLocations(context) {
|
|
318
|
+
return function recmaRestoreAuthoredJsxAttributeLocations() {
|
|
319
|
+
/** @param {unknown} tree */
|
|
320
|
+
return (tree) => {
|
|
321
|
+
const occurrences = [];
|
|
322
|
+
visitJsxAttributes(tree, (attribute) => occurrences.push(attribute));
|
|
323
|
+
const occurrenceCounts = new Map();
|
|
324
|
+
for (const attribute of occurrences) {
|
|
325
|
+
occurrenceCounts.set(attribute, (occurrenceCounts.get(attribute) ?? 0) + 1);
|
|
326
|
+
}
|
|
327
|
+
for (const attribute of occurrences) {
|
|
328
|
+
const name = attribute.name;
|
|
329
|
+
const location = context.attributeLocations.get(attribute);
|
|
330
|
+
if (
|
|
331
|
+
location &&
|
|
332
|
+
occurrenceCounts.get(attribute) === 1 &&
|
|
333
|
+
name?.type === 'JSXIdentifier' &&
|
|
334
|
+
name.name === location.name
|
|
335
|
+
) {
|
|
336
|
+
// Isolate the authored name from shallow attribute clones. Otherwise a
|
|
337
|
+
// generated clone can share this child object and regain its location
|
|
338
|
+
// when the original attribute is restored later in the traversal.
|
|
339
|
+
const restoredName = { ...name };
|
|
340
|
+
attribute.name = restoredName;
|
|
341
|
+
applyNodeLocation(restoredName, location);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
// Do not let a plugin-created clone carrying copied positions masquerade
|
|
345
|
+
// as the one authored attribute captured before user transforms.
|
|
346
|
+
for (const node of [attribute, name]) {
|
|
347
|
+
if (node === null || typeof node !== 'object') continue;
|
|
348
|
+
delete node.start;
|
|
349
|
+
delete node.end;
|
|
350
|
+
delete node.range;
|
|
351
|
+
delete node.loc;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
139
355
|
};
|
|
140
356
|
}
|
|
141
357
|
|
|
142
358
|
/**
|
|
143
359
|
* @param {string} jsxSource
|
|
144
360
|
* @param {unknown} mdxMap
|
|
361
|
+
* @param {string} authoredSource
|
|
145
362
|
* @param {string} id
|
|
146
363
|
* @param {CompileMdxOptions} options
|
|
364
|
+
* @param {DiagnosticMappingContext} diagnosticContext
|
|
147
365
|
* @returns {CompileMdxResult}
|
|
148
366
|
*/
|
|
149
|
-
function octaneStage(jsxSource, mdxMap, id, options) {
|
|
367
|
+
function octaneStage(jsxSource, mdxMap, authoredSource, id, options, diagnosticContext) {
|
|
150
368
|
const mode = options.mode ?? 'client';
|
|
151
369
|
const out = octaneCompile(jsxSource, id, {
|
|
152
370
|
mode,
|
|
153
371
|
hmr: mode === 'client' && !!options.hmr,
|
|
154
372
|
dev: mode === 'client' && !!options.dev,
|
|
373
|
+
profile: mode === 'client' && !!options.profile,
|
|
155
374
|
});
|
|
156
375
|
// Two-stage sourcemap: octane's map targets the INTERMEDIATE JSX text;
|
|
157
376
|
// @mdx-js/mdx's map (via SourceMapGenerator) targets the original .mdx.
|
|
@@ -165,6 +384,19 @@ function octaneStage(jsxSource, mdxMap, id, options) {
|
|
|
165
384
|
const chained = remapping([out.map, mdxMap], () => null);
|
|
166
385
|
if (String(chained.mappings).length > 0) out.map = chained;
|
|
167
386
|
}
|
|
387
|
+
// Compiler diagnostics are reported against the intermediate JSX emitted by
|
|
388
|
+
// MDX. Trace every available range through stage one's source map so direct
|
|
389
|
+
// callers and Vite warnings point at the JSX the author actually wrote in the
|
|
390
|
+
// document. Markdown generated by headings/lists has no authored event prop,
|
|
391
|
+
// so native-change warnings are expected to map only from literal MDX JSX.
|
|
392
|
+
out.diagnostics = remapDiagnostics(
|
|
393
|
+
out.diagnostics,
|
|
394
|
+
mdxMap,
|
|
395
|
+
jsxSource,
|
|
396
|
+
authoredSource,
|
|
397
|
+
id,
|
|
398
|
+
diagnosticContext.authoredAttributes,
|
|
399
|
+
);
|
|
168
400
|
// Fast refresh for documents: octane's compiler only auto-wraps EXPORTED
|
|
169
401
|
// `@{}`-form components in `hmr(...)` — `MDXContent` (a passthrough function
|
|
170
402
|
// returning a ternary of descriptors) isn't recognized as one, so the octane
|
|
@@ -185,9 +417,190 @@ function octaneStage(jsxSource, mdxMap, id, options) {
|
|
|
185
417
|
' });\n' +
|
|
186
418
|
'}\n';
|
|
187
419
|
}
|
|
420
|
+
// Register the final public binding after the MDX-specific HMR wrapper. The
|
|
421
|
+
// core compiler may also recognize the generated dispatcher, but its location
|
|
422
|
+
// belongs to intermediate JSX and its registration precedes this wrapper. This
|
|
423
|
+
// document-level registration deliberately overrides both with the authored
|
|
424
|
+
// `.mdx` identity and stable line-one location.
|
|
425
|
+
if (
|
|
426
|
+
mode === 'client' &&
|
|
427
|
+
options.profile &&
|
|
428
|
+
/\bexport default function MDXContent\b/.test(out.code)
|
|
429
|
+
) {
|
|
430
|
+
const metadata = {
|
|
431
|
+
id: `${id}#MDXContent@1:0`,
|
|
432
|
+
name: 'MDXContent',
|
|
433
|
+
file: id,
|
|
434
|
+
line: 1,
|
|
435
|
+
column: 0,
|
|
436
|
+
kind: 'component',
|
|
437
|
+
};
|
|
438
|
+
out.code +=
|
|
439
|
+
"\nimport { __profileComponent as _$mdxProfile } from 'octane/profiling';\n" +
|
|
440
|
+
`_$mdxProfile(MDXContent, ${JSON.stringify(metadata)});\n`;
|
|
441
|
+
}
|
|
188
442
|
return out;
|
|
189
443
|
}
|
|
190
444
|
|
|
445
|
+
/** @param {string} source */
|
|
446
|
+
function sourceLineStarts(source) {
|
|
447
|
+
const starts = [0];
|
|
448
|
+
for (let index = 0; index < source.length; index++) {
|
|
449
|
+
if (source.charCodeAt(index) === 10) starts.push(index + 1);
|
|
450
|
+
}
|
|
451
|
+
return starts;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @param {string} source
|
|
456
|
+
* @param {number[]} starts
|
|
457
|
+
* @param {number} offset
|
|
458
|
+
*/
|
|
459
|
+
function positionForOffset(source, starts, offset) {
|
|
460
|
+
const clamped = Math.max(0, Math.min(source.length, offset));
|
|
461
|
+
let low = 0;
|
|
462
|
+
let high = starts.length;
|
|
463
|
+
while (low + 1 < high) {
|
|
464
|
+
const mid = (low + high) >> 1;
|
|
465
|
+
if (starts[mid] <= clamped) low = mid;
|
|
466
|
+
else high = mid;
|
|
467
|
+
}
|
|
468
|
+
return { offset: clamped, line: low + 1, column: clamped - starts[low] };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* @param {TraceMap} trace
|
|
473
|
+
* @param {string} source
|
|
474
|
+
* @param {number[]} starts
|
|
475
|
+
* @param {{ offset: number, line: number, column: number }} position
|
|
476
|
+
* @param {number} bias
|
|
477
|
+
*/
|
|
478
|
+
function mapDiagnosticPosition(trace, source, starts, position, bias) {
|
|
479
|
+
const mapped = originalPositionFor(trace, {
|
|
480
|
+
line: position.line,
|
|
481
|
+
column: position.column,
|
|
482
|
+
bias,
|
|
483
|
+
});
|
|
484
|
+
if (mapped.line == null || mapped.column == null) return null;
|
|
485
|
+
const lineStart = starts[mapped.line - 1];
|
|
486
|
+
if (lineStart === undefined) return null;
|
|
487
|
+
return positionForOffset(source, starts, lineStart + mapped.column);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Match an Octane diagnostic to an exact authored JSX attribute source-map
|
|
492
|
+
* segment. Both source-map biases must resolve to the same seeded token: when
|
|
493
|
+
* they differ, the generated position sits between mappings and belongs to
|
|
494
|
+
* transformed/generated code rather than an authored attribute.
|
|
495
|
+
*
|
|
496
|
+
* @param {TraceMap} trace
|
|
497
|
+
* @param {string} jsxSource
|
|
498
|
+
* @param {string} source
|
|
499
|
+
* @param {number[]} starts
|
|
500
|
+
* @param {{ start: { offset: number, line: number, column: number }, end: { offset: number, line: number, column: number } }} range
|
|
501
|
+
* @param {AuthoredJsxAttributeLocation[]} authoredAttributes
|
|
502
|
+
* @returns {AuthoredJsxAttributeLocation | null}
|
|
503
|
+
*/
|
|
504
|
+
function exactAuthoredAttributeForDiagnostic(
|
|
505
|
+
trace,
|
|
506
|
+
jsxSource,
|
|
507
|
+
source,
|
|
508
|
+
starts,
|
|
509
|
+
range,
|
|
510
|
+
authoredAttributes,
|
|
511
|
+
) {
|
|
512
|
+
const name = jsxSource.slice(range.start.offset, range.end.offset);
|
|
513
|
+
if (name.length === 0) return null;
|
|
514
|
+
const lower = mapDiagnosticPosition(trace, source, starts, range.start, GREATEST_LOWER_BOUND);
|
|
515
|
+
const upper = mapDiagnosticPosition(trace, source, starts, range.start, LEAST_UPPER_BOUND);
|
|
516
|
+
if (lower === null || upper === null || lower.offset !== upper.offset) return null;
|
|
517
|
+
return (
|
|
518
|
+
authoredAttributes.find(
|
|
519
|
+
(attribute) =>
|
|
520
|
+
attribute.staticallyWarned && attribute.name === name && attribute.start === lower.offset,
|
|
521
|
+
) ?? null
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* @param {string} source
|
|
527
|
+
* @param {number[]} starts
|
|
528
|
+
* @param {AuthoredJsxAttributeLocation} attribute
|
|
529
|
+
*/
|
|
530
|
+
function rangeForAuthoredAttribute(source, starts, attribute) {
|
|
531
|
+
return {
|
|
532
|
+
start: positionForOffset(source, starts, attribute.start),
|
|
533
|
+
end: positionForOffset(source, starts, attribute.end),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* @param {CompileMdxResult['diagnostics'] | undefined} diagnostics
|
|
539
|
+
* @param {unknown} mdxMap
|
|
540
|
+
* @param {string} jsxSource
|
|
541
|
+
* @param {string} source
|
|
542
|
+
* @param {string} id
|
|
543
|
+
* @param {AuthoredJsxAttributeLocation[]} authoredAttributes
|
|
544
|
+
* @returns {CompileMdxResult['diagnostics']}
|
|
545
|
+
*/
|
|
546
|
+
function remapDiagnostics(diagnostics, mdxMap, jsxSource, source, id, authoredAttributes) {
|
|
547
|
+
if (!Array.isArray(diagnostics) || diagnostics.length === 0) return [];
|
|
548
|
+
const starts = sourceLineStarts(source);
|
|
549
|
+
const fileStart = positionForOffset(source, starts, 0);
|
|
550
|
+
const trace = mdxMap ? new TraceMap(JSON.parse(JSON.stringify(mdxMap))) : null;
|
|
551
|
+
const claimedAttributes = new Set();
|
|
552
|
+
return diagnostics.map((diagnostic) => {
|
|
553
|
+
const attribute = trace
|
|
554
|
+
? exactAuthoredAttributeForDiagnostic(
|
|
555
|
+
trace,
|
|
556
|
+
jsxSource,
|
|
557
|
+
source,
|
|
558
|
+
starts,
|
|
559
|
+
diagnostic,
|
|
560
|
+
authoredAttributes,
|
|
561
|
+
)
|
|
562
|
+
: null;
|
|
563
|
+
const key = attribute ? `${attribute.name}:${attribute.start}:${attribute.end}` : null;
|
|
564
|
+
if (attribute === null || key === null || claimedAttributes.has(key)) {
|
|
565
|
+
return {
|
|
566
|
+
...diagnostic,
|
|
567
|
+
filename: id,
|
|
568
|
+
start: fileStart,
|
|
569
|
+
end: fileStart,
|
|
570
|
+
suggestions: [],
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
claimedAttributes.add(key);
|
|
574
|
+
const range = rangeForAuthoredAttribute(source, starts, attribute);
|
|
575
|
+
const claimedSuggestions = new Set();
|
|
576
|
+
return {
|
|
577
|
+
...diagnostic,
|
|
578
|
+
filename: id,
|
|
579
|
+
...range,
|
|
580
|
+
suggestions: diagnostic.suggestions.flatMap((suggestion) => {
|
|
581
|
+
const suggestionAttribute = exactAuthoredAttributeForDiagnostic(
|
|
582
|
+
trace,
|
|
583
|
+
jsxSource,
|
|
584
|
+
source,
|
|
585
|
+
starts,
|
|
586
|
+
suggestion,
|
|
587
|
+
authoredAttributes,
|
|
588
|
+
);
|
|
589
|
+
if (suggestionAttribute === null) return [];
|
|
590
|
+
const suggestionKey = `${suggestionAttribute.name}:${suggestionAttribute.start}:${suggestionAttribute.end}`;
|
|
591
|
+
if (claimedSuggestions.has(suggestionKey)) return [];
|
|
592
|
+
claimedSuggestions.add(suggestionKey);
|
|
593
|
+
return [
|
|
594
|
+
{
|
|
595
|
+
...suggestion,
|
|
596
|
+
...rangeForAuthoredAttribute(source, starts, suggestionAttribute),
|
|
597
|
+
},
|
|
598
|
+
];
|
|
599
|
+
}),
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
191
604
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
605
|
// recmaOctaneAdapter — the ESTree pass described in the module doc.
|
|
193
606
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/src/octane-compiler.d.ts
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
// `octane/compiler` is authored in JSDoc'd JS with no shipped declarations —
|
|
2
|
-
// a minimal ambient surface for the
|
|
2
|
+
// a minimal ambient surface for the entry points this package consumes.
|
|
3
3
|
declare module 'octane/compiler' {
|
|
4
|
+
export interface CompileDiagnosticPosition {
|
|
5
|
+
offset: number;
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CompileDiagnostic {
|
|
10
|
+
code: string;
|
|
11
|
+
severity: 'warning';
|
|
12
|
+
message: string;
|
|
13
|
+
filename: string;
|
|
14
|
+
start: CompileDiagnosticPosition;
|
|
15
|
+
end: CompileDiagnosticPosition;
|
|
16
|
+
suggestions: Array<{
|
|
17
|
+
start: CompileDiagnosticPosition;
|
|
18
|
+
end: CompileDiagnosticPosition;
|
|
19
|
+
attribute: 'onInput' | 'onInputCapture';
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
4
22
|
export function compile(
|
|
5
23
|
source: string,
|
|
6
24
|
id: string,
|
|
@@ -8,6 +26,12 @@ declare module 'octane/compiler' {
|
|
|
8
26
|
mode?: 'client' | 'server';
|
|
9
27
|
hmr?: boolean;
|
|
10
28
|
dev?: boolean;
|
|
29
|
+
profile?: boolean;
|
|
11
30
|
},
|
|
12
|
-
): { code: string; map: unknown };
|
|
31
|
+
): { code: string; map: unknown; diagnostics: CompileDiagnostic[] };
|
|
32
|
+
export function __analyzeNativeChangeDiagnostics(
|
|
33
|
+
ast: unknown,
|
|
34
|
+
source: string,
|
|
35
|
+
filename: string,
|
|
36
|
+
): { diagnostics: CompileDiagnostic[]; classifications: Map<number, string> };
|
|
13
37
|
}
|
package/src/vite.js
CHANGED
|
@@ -20,12 +20,14 @@
|
|
|
20
20
|
* exactly as written and never applies TS-style `.js` → `.ts` mapping.
|
|
21
21
|
*/
|
|
22
22
|
import { compileMdx } from './compile.js';
|
|
23
|
+
import { createOctaneCompiler } from 'octane/compiler/bundler';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* @typedef {Omit<import('./compile.js').CompileMdxOptions, 'mode' | 'hmr' | 'dev'> & {
|
|
26
27
|
* ssr?: boolean,
|
|
27
28
|
* md?: boolean,
|
|
28
29
|
* hmr?: boolean,
|
|
30
|
+
* profile?: boolean,
|
|
29
31
|
* }} OctaneMdxPluginOptions
|
|
30
32
|
*
|
|
31
33
|
* `ssr` forces the codegen target for EVERY module — `true` always server,
|
|
@@ -34,6 +36,7 @@ import { compileMdx } from './compile.js';
|
|
|
34
36
|
* `md` also transforms `.md` modules (plain-markdown format). Default `true`.
|
|
35
37
|
* `hmr` is the octane HMR/dev metadata override; defaults to on in serve mode
|
|
36
38
|
* (client only).
|
|
39
|
+
* `profile` enables profiling metadata in client modules only.
|
|
37
40
|
*/
|
|
38
41
|
|
|
39
42
|
/**
|
|
@@ -44,8 +47,9 @@ import { compileMdx } from './compile.js';
|
|
|
44
47
|
* @typedef {{
|
|
45
48
|
* name: string,
|
|
46
49
|
* enforce: 'pre',
|
|
47
|
-
* configResolved(config: { command: string }): void,
|
|
48
|
-
*
|
|
50
|
+
* configResolved(config: { command: string, root?: string }): void,
|
|
51
|
+
* watchChange(id: string): void,
|
|
52
|
+
* transform(this: { addWatchFile?(id: string): void, warn?(warning: { code: string, message: string, id: string, loc: { file: string, line: number, column: number } }): void, environment?: { config?: { consumer?: string } } }, code: string, id: string, options?: { ssr?: boolean }): Promise<import('./compile.js').CompileMdxResult | null>,
|
|
49
53
|
* }} OctaneMdxPlugin
|
|
50
54
|
*/
|
|
51
55
|
|
|
@@ -54,15 +58,24 @@ import { compileMdx } from './compile.js';
|
|
|
54
58
|
* @returns {OctaneMdxPlugin}
|
|
55
59
|
*/
|
|
56
60
|
export function octaneMdx(options = {}) {
|
|
57
|
-
const { ssr: forceSsr, md, hmr, ...compileOptions } = options;
|
|
61
|
+
const { ssr: forceSsr, md, hmr, profile, ...compileOptions } = options;
|
|
58
62
|
let hmrEnabled = hmr;
|
|
63
|
+
let projectRoot = process.cwd();
|
|
64
|
+
let profileIds = createOctaneCompiler({ root: projectRoot });
|
|
65
|
+
const warnedByFile = new Map();
|
|
59
66
|
const includeMd = md !== false;
|
|
60
67
|
return {
|
|
61
68
|
name: 'octane-mdx',
|
|
62
69
|
enforce: 'pre',
|
|
63
70
|
configResolved(config) {
|
|
71
|
+
projectRoot = config.root ?? projectRoot;
|
|
72
|
+
profileIds = createOctaneCompiler({ root: projectRoot });
|
|
64
73
|
if (hmrEnabled === undefined) hmrEnabled = config.command === 'serve';
|
|
65
74
|
},
|
|
75
|
+
watchChange(id) {
|
|
76
|
+
profileIds.invalidate(id);
|
|
77
|
+
warnedByFile.delete(id.split('?')[0]);
|
|
78
|
+
},
|
|
66
79
|
async transform(code, id, transformOptions) {
|
|
67
80
|
const [file, query = ''] = id.split('?'); // Vite ids carry ?v=/?used/?raw/… suffixes
|
|
68
81
|
if (!(file.endsWith('.mdx') || (includeMd && file.endsWith('.md')))) return null;
|
|
@@ -76,12 +89,43 @@ export function octaneMdx(options = {}) {
|
|
|
76
89
|
forceSsr !== undefined
|
|
77
90
|
? forceSsr
|
|
78
91
|
: transformOptions?.ssr === true || this.environment?.config?.consumer === 'server';
|
|
79
|
-
|
|
92
|
+
const profiling = !ssr && profile === true;
|
|
93
|
+
// Preserve the historical filename (and therefore output/source maps) in
|
|
94
|
+
// ordinary and server builds. Profile metadata needs the portable ID, so
|
|
95
|
+
// only the explicit client profiling specialization canonicalizes it.
|
|
96
|
+
const profileIdentity = profiling ? profileIds.resolveProfileModuleId(file) : null;
|
|
97
|
+
for (const dependency of profileIdentity?.dependencies ?? []) {
|
|
98
|
+
this.addWatchFile?.(dependency);
|
|
99
|
+
}
|
|
100
|
+
const compilerId = profileIdentity?.id ?? file;
|
|
101
|
+
const result = await compileMdx(code, compilerId, {
|
|
80
102
|
...compileOptions,
|
|
81
103
|
mode: ssr ? 'server' : 'client',
|
|
82
104
|
hmr: !ssr && !!hmrEnabled,
|
|
83
105
|
dev: !ssr && !!hmrEnabled,
|
|
106
|
+
profile: profiling,
|
|
84
107
|
});
|
|
108
|
+
let warned = warnedByFile.get(file);
|
|
109
|
+
if (warned === undefined) {
|
|
110
|
+
warned = new Set();
|
|
111
|
+
warnedByFile.set(file, warned);
|
|
112
|
+
}
|
|
113
|
+
for (const diagnostic of result.diagnostics) {
|
|
114
|
+
const key = `${diagnostic.code}:${diagnostic.start.offset}:${diagnostic.end.offset}:${diagnostic.message}`;
|
|
115
|
+
if (warned.has(key)) continue;
|
|
116
|
+
warned.add(key);
|
|
117
|
+
this.warn?.({
|
|
118
|
+
code: diagnostic.code,
|
|
119
|
+
message: diagnostic.message,
|
|
120
|
+
id: diagnostic.filename,
|
|
121
|
+
loc: {
|
|
122
|
+
file: diagnostic.filename,
|
|
123
|
+
line: diagnostic.start.line,
|
|
124
|
+
column: diagnostic.start.column,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
85
129
|
},
|
|
86
130
|
};
|
|
87
131
|
}
|
package/types/compile.d.ts
CHANGED
|
@@ -6,6 +6,27 @@ import type { CompileOptions } from '@mdx-js/mdx';
|
|
|
6
6
|
export interface CompileMdxResult {
|
|
7
7
|
code: string;
|
|
8
8
|
map: unknown;
|
|
9
|
+
diagnostics: CompileMdxDiagnostic[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CompileMdxDiagnosticPosition {
|
|
13
|
+
offset: number;
|
|
14
|
+
line: number;
|
|
15
|
+
column: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CompileMdxDiagnostic {
|
|
19
|
+
code: string;
|
|
20
|
+
severity: 'warning';
|
|
21
|
+
message: string;
|
|
22
|
+
filename: string;
|
|
23
|
+
start: CompileMdxDiagnosticPosition;
|
|
24
|
+
end: CompileMdxDiagnosticPosition;
|
|
25
|
+
suggestions: Array<{
|
|
26
|
+
start: CompileMdxDiagnosticPosition;
|
|
27
|
+
end: CompileMdxDiagnosticPosition;
|
|
28
|
+
attribute: 'onInput' | 'onInputCapture';
|
|
29
|
+
}>;
|
|
9
30
|
}
|
|
10
31
|
|
|
11
32
|
export interface CompileMdxOptions {
|
|
@@ -15,6 +36,8 @@ export interface CompileMdxOptions {
|
|
|
15
36
|
hmr?: boolean;
|
|
16
37
|
/** octane compiler dev metadata (client only; same gate as `hmr`). */
|
|
17
38
|
dev?: boolean;
|
|
39
|
+
/** octane compiler profiling metadata (client only). */
|
|
40
|
+
profile?: boolean;
|
|
18
41
|
/**
|
|
19
42
|
* Module the emitted document reads the provider mapping from
|
|
20
43
|
* (`useMDXComponents`). Defaults per mode — `'@octanejs/mdx'` (client) /
|
package/types/vite.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Hand-written declarations for src/vite.js (authored in `.js` so it loads
|
|
2
2
|
// from a consumer's vite.config.ts under Node's native ESM loader — same
|
|
3
3
|
// convention as @octanejs/vite-plugin's types/). Keep in lockstep.
|
|
4
|
-
import type { CompileMdxOptions } from './compile.js';
|
|
4
|
+
import type { CompileMdxOptions, CompileMdxResult } from './compile.js';
|
|
5
5
|
|
|
6
6
|
export interface OctaneMdxPluginOptions extends Omit<CompileMdxOptions, 'mode' | 'hmr' | 'dev'> {
|
|
7
7
|
/**
|
|
@@ -14,6 +14,8 @@ export interface OctaneMdxPluginOptions extends Omit<CompileMdxOptions, 'mode' |
|
|
|
14
14
|
md?: boolean;
|
|
15
15
|
/** octane HMR/dev metadata override; defaults to on in serve mode (client only). */
|
|
16
16
|
hmr?: boolean;
|
|
17
|
+
/** Enable component profiling metadata in client modules. */
|
|
18
|
+
profile?: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -23,13 +25,23 @@ export interface OctaneMdxPluginOptions extends Omit<CompileMdxOptions, 'mode' |
|
|
|
23
25
|
export interface OctaneMdxPlugin {
|
|
24
26
|
name: string;
|
|
25
27
|
enforce: 'pre';
|
|
26
|
-
configResolved(config: { command: string }): void;
|
|
28
|
+
configResolved(config: { command: string; root?: string }): void;
|
|
29
|
+
watchChange(id: string): void;
|
|
27
30
|
transform(
|
|
28
|
-
this:
|
|
31
|
+
this: {
|
|
32
|
+
addWatchFile?(id: string): void;
|
|
33
|
+
warn?(warning: {
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
id: string;
|
|
37
|
+
loc: { file: string; line: number; column: number };
|
|
38
|
+
}): void;
|
|
39
|
+
environment?: { config?: { consumer?: string } };
|
|
40
|
+
},
|
|
29
41
|
code: string,
|
|
30
42
|
id: string,
|
|
31
43
|
options?: { ssr?: boolean },
|
|
32
|
-
): Promise<
|
|
44
|
+
): Promise<CompileMdxResult | null>;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
export declare function octaneMdx(options?: OctaneMdxPluginOptions): OctaneMdxPlugin;
|