@uniflowed/vite 0.0.0-alpha.1 → 0.0.0-alpha.10
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/driver.js +932 -93
- package/index.js +384 -26
- package/internal/assets.js +396 -0
- package/internal/config.js +22 -7
- package/internal/events.js +22 -0
- package/internal/flow-grammar-shim.js +241 -0
- package/internal/flow-keywords.js +505 -0
- package/internal/highlight.js +181 -0
- package/internal/http.js +79 -0
- package/internal/refresh-runtime.js +221 -246
- package/internal/refresh.js +2 -0
- package/internal/routes.js +391 -34
- package/internal/rsc.js +151 -0
- package/internal/serve.js +345 -0
- package/merge.js +87 -0
- package/package.json +10 -10
- package/bun-preload.js +0 -22
- package/internal/node-hooks.js +0 -111
- package/register.js +0 -11
- package/transform.js +0 -187
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// The Flow syntax a JavaScript grammar cannot parse, shown to it as
|
|
4
|
+
// JavaScript it can, and taken back afterwards.
|
|
5
|
+
//
|
|
6
|
+
// # Why re-tagging the word is not enough
|
|
7
|
+
//
|
|
8
|
+
// `internal/flow-keywords.js` colours Flow's words after the grammar has run.
|
|
9
|
+
// That works for a word the grammar tokenised and mis-labelled. It cannot work
|
|
10
|
+
// for syntax that stops the grammar, because there are then no tokens to
|
|
11
|
+
// re-label — the whole construct arrives as one unstyled run. Two pieces of
|
|
12
|
+
// Flow do that, and both are common enough that this repository's own
|
|
13
|
+
// documentation hit them on its first page about Flow.
|
|
14
|
+
//
|
|
15
|
+
// **A `component` or `hook` declaration.** A JavaScript grammar has no
|
|
16
|
+
// production for an identifier where a declaration keyword belongs, so it
|
|
17
|
+
// gives up on the rest of the line:
|
|
18
|
+
//
|
|
19
|
+
// export component Avatar(src: string, size: number = 32) {
|
|
20
|
+
// └ keyword ┘ └ name ┘ └───────── one grey token ─────────┘
|
|
21
|
+
//
|
|
22
|
+
// The parameter names, their types and the default value were all the same
|
|
23
|
+
// undifferentiated grey. `export hook useNow(…) {` was worse: the grammar's
|
|
24
|
+
// state did not recover, and all eight lines of that sample came out as one
|
|
25
|
+
// grey token each — a code block with no highlighting at all, on the page
|
|
26
|
+
// whose subject is Flow's syntax.
|
|
27
|
+
//
|
|
28
|
+
// **An exact object type, `{| … |}`.** This one is quieter and travels
|
|
29
|
+
// further. The grammar reads the `{|` as a brace and a bitwise or, and every
|
|
30
|
+
// line *after* it in the same block is then mis-scoped: `export` came out in
|
|
31
|
+
// the colour of a function call, `return` likewise, and a JSX tag lost its
|
|
32
|
+
// element colour. One type annotation discoloured the rest of the sample.
|
|
33
|
+
//
|
|
34
|
+
// # What this does instead
|
|
35
|
+
//
|
|
36
|
+
// It hands the grammar `function` where the source says `component` or `hook`,
|
|
37
|
+
// and a plain brace where the source says `{|`, takes the tokens that
|
|
38
|
+
// produces, and rebuilds them over the original text. The grammar then walks
|
|
39
|
+
// the parameter list, the return type and the body the way it does for any
|
|
40
|
+
// function, and `flow-keywords.js` recolours the restored words.
|
|
41
|
+
//
|
|
42
|
+
// # Why this cannot corrupt the sample
|
|
43
|
+
//
|
|
44
|
+
// Because {@link restoreLine} never copies from the text the grammar saw. It
|
|
45
|
+
// maps each token's boundaries back into the original line and slices *that*,
|
|
46
|
+
// so the concatenation of a restored line is the original line by
|
|
47
|
+
// construction, whatever the grammar decided to do with a stand-in. A
|
|
48
|
+
// mis-shimmed line can come out with the wrong colours. It cannot come out
|
|
49
|
+
// saying `function`.
|
|
50
|
+
//
|
|
51
|
+
// # What it does not cover
|
|
52
|
+
//
|
|
53
|
+
// The declaration rewrite is anchored to the start of a line, after an
|
|
54
|
+
// optional `export` or `export default`, which is where a declaration begins
|
|
55
|
+
// and where the grammar breaks. A declaration written anywhere else is left to
|
|
56
|
+
// the ordinary path.
|
|
57
|
+
//
|
|
58
|
+
// Neither rewrite asks whether the line is inside a string or a comment, so a
|
|
59
|
+
// line of quoted sample code that opens with `component Name(` is rewritten
|
|
60
|
+
// too. The text still survives exactly; only its colours are a function's
|
|
61
|
+
// rather than a string's, and `flow-keywords.js` still refuses to call the
|
|
62
|
+
// word a keyword. Buying the remaining fidelity would mean lexing the block
|
|
63
|
+
// twice, once here and once there, to fix a case that is a code sample inside
|
|
64
|
+
// a code sample.
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A `component` or `hook` declaration head at the start of a line.
|
|
68
|
+
*
|
|
69
|
+
* The name and the opening bracket are matched but not captured: requiring
|
|
70
|
+
* them is what distinguishes a declaration from `const component = 1`, and
|
|
71
|
+
* consuming them would mean putting them back.
|
|
72
|
+
*/
|
|
73
|
+
const DECLARATION_HEAD =
|
|
74
|
+
/^([ \t]*(?:export[ \t]+(?:default[ \t]+)?)?)(component|hook)(?=[ \t]+[A-Za-z_$][\w$]*[ \t]*[(<])/;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The word a declaration keyword is shown as.
|
|
78
|
+
*
|
|
79
|
+
* `function` and not `function*`: a generator tokenises identically here, and
|
|
80
|
+
* the length no longer has to match now that restoration maps positions rather
|
|
81
|
+
* than assuming they line up.
|
|
82
|
+
*/
|
|
83
|
+
const DECLARATION_STAND_IN = "function";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The braces of an exact object type, and the ordinary braces they are shown
|
|
87
|
+
* as.
|
|
88
|
+
*
|
|
89
|
+
* Same length in both directions, so the rest of the line does not move; the
|
|
90
|
+
* mapping would cope either way, but a rewrite that cannot shift anything is
|
|
91
|
+
* one less thing to reason about. `{||}` — the empty exact object — is two
|
|
92
|
+
* adjacent rewrites rather than an overlapping one, which is why these are
|
|
93
|
+
* matched as a pair of two-character sequences rather than as one bracket.
|
|
94
|
+
*/
|
|
95
|
+
const EXACT_OBJECT = /\{\||\|\}/g;
|
|
96
|
+
|
|
97
|
+
const EXACT_OBJECT_STAND_INS = { "{|": "{ ", "|}": " }" };
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `source` rewritten for the grammar, with the undo that belongs to it.
|
|
101
|
+
*
|
|
102
|
+
* The undo is returned rather than exported separately because it closes over
|
|
103
|
+
* the original lines, and pairing the wrong undo with a rewrite is the one
|
|
104
|
+
* mistake that would matter. `restore` is the identity when nothing was
|
|
105
|
+
* rewritten, so the caller has no case to distinguish.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} source
|
|
108
|
+
* @returns {{code: string, restore: (lines: Array<Array<object>>) => Array<Array<object>>}}
|
|
109
|
+
*/
|
|
110
|
+
export function shimFlowGrammar(source) {
|
|
111
|
+
const original = source.split("\n");
|
|
112
|
+
const edits = new Map();
|
|
113
|
+
|
|
114
|
+
const shimmed = original.map((line, index) => {
|
|
115
|
+
const lineEdits = editsFor(line);
|
|
116
|
+
if (lineEdits.length === 0) {
|
|
117
|
+
return line;
|
|
118
|
+
}
|
|
119
|
+
edits.set(index, lineEdits);
|
|
120
|
+
return rewrite(line, lineEdits);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (edits.size === 0) {
|
|
124
|
+
return { code: source, restore: (lines) => lines };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
code: shimmed.join("\n"),
|
|
128
|
+
restore: (lines) => restore(lines, edits, original),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Every rewrite one line needs, in the order they occur.
|
|
134
|
+
*
|
|
135
|
+
* Ordered and non-overlapping, because {@link mapping} walks them once and
|
|
136
|
+
* accumulates the shift each one makes. The declaration head is found first
|
|
137
|
+
* and starts at the line's indentation, so it can never overlap an exact
|
|
138
|
+
* object brace, which needs a `{` or a `|`.
|
|
139
|
+
*/
|
|
140
|
+
function editsFor(line) {
|
|
141
|
+
const found = [];
|
|
142
|
+
const head = DECLARATION_HEAD.exec(line);
|
|
143
|
+
if (head != null) {
|
|
144
|
+
found.push({ column: head[1].length, text: head[2], standIn: DECLARATION_STAND_IN });
|
|
145
|
+
}
|
|
146
|
+
for (const brace of line.matchAll(EXACT_OBJECT)) {
|
|
147
|
+
found.push({
|
|
148
|
+
column: brace.index ?? 0,
|
|
149
|
+
text: brace[0],
|
|
150
|
+
standIn: EXACT_OBJECT_STAND_INS[brace[0]],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** `line` with every rewrite applied, left to right. */
|
|
157
|
+
function rewrite(line, edits) {
|
|
158
|
+
let out = "";
|
|
159
|
+
let at = 0;
|
|
160
|
+
for (const edit of edits) {
|
|
161
|
+
out += line.slice(at, edit.column) + edit.standIn;
|
|
162
|
+
at = edit.column + edit.text.length;
|
|
163
|
+
}
|
|
164
|
+
return out + line.slice(at);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The tokenised block, rebuilt over the original source.
|
|
169
|
+
*
|
|
170
|
+
* Offsets are recomputed rather than adjusted: a stand-in that is not the
|
|
171
|
+
* length of the text it stands for moves everything after it on the line, and
|
|
172
|
+
* every line after that one, so there is no correct delta to add. Walking the
|
|
173
|
+
* restored lines gives the right answer directly.
|
|
174
|
+
*/
|
|
175
|
+
function restore(lines, edits, original) {
|
|
176
|
+
let offset = 0;
|
|
177
|
+
return lines.map((tokens, index) => {
|
|
178
|
+
const text = original[index] ?? tokens.map((token) => token.content).join("");
|
|
179
|
+
const lineEdits = edits.get(index);
|
|
180
|
+
const restored =
|
|
181
|
+
lineEdits == null ? reoffset(tokens, offset) : restoreLine(tokens, text, lineEdits, offset);
|
|
182
|
+
offset += text.length + 1;
|
|
183
|
+
return restored;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A line the grammar saw unchanged, with its offsets moved to the original. */
|
|
188
|
+
function reoffset(tokens, offset) {
|
|
189
|
+
let at = 0;
|
|
190
|
+
return tokens.map((token) => {
|
|
191
|
+
const placed = { ...token, offset: offset + at };
|
|
192
|
+
at += token.content.length;
|
|
193
|
+
return placed;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One rewritten line's tokens, re-cut over the original text.
|
|
199
|
+
*
|
|
200
|
+
* Every token boundary is a position in the line the grammar saw; {@link
|
|
201
|
+
* mapping} turns it into a position in the line the reader gets. A boundary
|
|
202
|
+
* that falls *inside* a stand-in maps to the end of the text it stands for, so
|
|
203
|
+
* that text always lands in exactly one piece and no character of the line is
|
|
204
|
+
* dropped — the pieces still tile the line end to end even when the grammar
|
|
205
|
+
* split a stand-in or swallowed it into a longer token.
|
|
206
|
+
*/
|
|
207
|
+
function restoreLine(tokens, text, edits, offset) {
|
|
208
|
+
const map = mapping(edits);
|
|
209
|
+
const out = [];
|
|
210
|
+
let seen = 0;
|
|
211
|
+
let at = 0;
|
|
212
|
+
for (const token of tokens) {
|
|
213
|
+
const from = map(seen);
|
|
214
|
+
seen += token.content.length;
|
|
215
|
+
const to = map(seen);
|
|
216
|
+
if (to <= from) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
out.push({ ...token, content: text.slice(from, to), offset: offset + at });
|
|
220
|
+
at += to - from;
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A position in the rewritten line, as a position in the original one. */
|
|
226
|
+
function mapping(edits) {
|
|
227
|
+
return (at) => {
|
|
228
|
+
let shift = 0;
|
|
229
|
+
for (const edit of edits) {
|
|
230
|
+
const from = edit.column + shift;
|
|
231
|
+
if (at <= from) {
|
|
232
|
+
return at - shift;
|
|
233
|
+
}
|
|
234
|
+
if (at < from + edit.standIn.length) {
|
|
235
|
+
return edit.column + edit.text.length;
|
|
236
|
+
}
|
|
237
|
+
shift += edit.standIn.length - edit.text.length;
|
|
238
|
+
}
|
|
239
|
+
return at - shift;
|
|
240
|
+
};
|
|
241
|
+
}
|