@effected/workspaces 0.25.0 → 0.26.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/LayerPolicy.js +133 -0
- package/PackedInstall.js +282 -0
- package/README.md +195 -0
- package/SourceBoundary.js +540 -0
- package/WorkspaceLayering.js +199 -0
- package/internal/dependencyFields.js +17 -0
- package/internal/packedInstallPlan.js +174 -0
- package/internal/sourceText.js +333 -0
- package/package.json +6 -1
- package/testing.d.ts +1382 -0
- package/testing.js +10 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
//#region src/internal/sourceText.ts
|
|
2
|
+
const IDENTIFIER = /[\p{ID_Continue}$\u200c\u200d]/u;
|
|
3
|
+
const SPACE = /\s/;
|
|
4
|
+
const FLAG = /[a-z]/i;
|
|
5
|
+
/** Keywords after which a `/` opens a regex literal rather than a division. */
|
|
6
|
+
const REGEX_AFTER = /* @__PURE__ */ new Set([
|
|
7
|
+
"return",
|
|
8
|
+
"typeof",
|
|
9
|
+
"instanceof",
|
|
10
|
+
"in",
|
|
11
|
+
"of",
|
|
12
|
+
"new",
|
|
13
|
+
"delete",
|
|
14
|
+
"void",
|
|
15
|
+
"throw",
|
|
16
|
+
"case",
|
|
17
|
+
"do",
|
|
18
|
+
"else",
|
|
19
|
+
"yield",
|
|
20
|
+
"await"
|
|
21
|
+
]);
|
|
22
|
+
/** Keywords whose parenthesized condition may be followed directly by a statement, so a `/` after its `)` opens a regex. */
|
|
23
|
+
const CONTROL = /* @__PURE__ */ new Set([
|
|
24
|
+
"if",
|
|
25
|
+
"while",
|
|
26
|
+
"for",
|
|
27
|
+
"with"
|
|
28
|
+
]);
|
|
29
|
+
/** Objects a member access on which still reaches a global. */
|
|
30
|
+
const GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
|
|
31
|
+
"globalThis",
|
|
32
|
+
"global",
|
|
33
|
+
"window",
|
|
34
|
+
"self"
|
|
35
|
+
]);
|
|
36
|
+
/** Whether `char` can continue an identifier. */
|
|
37
|
+
const isIdentifierChar = (char) => char !== void 0 && IDENTIFIER.test(char);
|
|
38
|
+
/** Lex `text` once into its comment-free view, its code-only view and its literals. */
|
|
39
|
+
const lex = (text) => {
|
|
40
|
+
const kept = [];
|
|
41
|
+
const code = [];
|
|
42
|
+
const literals = [];
|
|
43
|
+
const substitutions = [];
|
|
44
|
+
const parens = [];
|
|
45
|
+
const controlCloses = /* @__PURE__ */ new Set();
|
|
46
|
+
const length = text.length;
|
|
47
|
+
let i = 0;
|
|
48
|
+
const blank = (char) => char === "\n" || char === "\r" ? char : " ";
|
|
49
|
+
/** Emit one input character: `keep` for the comment-free view, `live` for the code view. */
|
|
50
|
+
const emit = (char, keep, live) => {
|
|
51
|
+
kept.push(keep ? char : blank(char));
|
|
52
|
+
code.push(live ? char : blank(char));
|
|
53
|
+
};
|
|
54
|
+
/** The last non-space offset in the code view at or before `from`; `-1` when there is none. */
|
|
55
|
+
const lastCode = (from) => {
|
|
56
|
+
let j = from;
|
|
57
|
+
while (j >= 0 && SPACE.test(code[j] ?? "")) j--;
|
|
58
|
+
return j;
|
|
59
|
+
};
|
|
60
|
+
/** The identifier ending at code offset `end`, or `""`. */
|
|
61
|
+
const wordAt = (end) => {
|
|
62
|
+
let start = end;
|
|
63
|
+
while (start >= 0 && isIdentifierChar(code[start])) start--;
|
|
64
|
+
return code.slice(start + 1, end + 1).join("");
|
|
65
|
+
};
|
|
66
|
+
/** Whether the code at offset `j` ends an operand (a value a postfix operator or a `/` division can follow). */
|
|
67
|
+
const endsOperand = (j) => {
|
|
68
|
+
const char = code[j] ?? "";
|
|
69
|
+
return char === ")" || char === "]" || isIdentifierChar(char) && !REGEX_AFTER.has(wordAt(j));
|
|
70
|
+
};
|
|
71
|
+
/** Whether a `/` here opens a regex, judged by the last code token. */
|
|
72
|
+
const regexAllowed = () => {
|
|
73
|
+
const j = lastCode(code.length - 1);
|
|
74
|
+
if (j < 0) return true;
|
|
75
|
+
const last = code[j] ?? "";
|
|
76
|
+
if (isIdentifierChar(last)) return REGEX_AFTER.has(wordAt(j));
|
|
77
|
+
if (last === "!") return !endsOperand(j - 1);
|
|
78
|
+
if ((last === "+" || last === "-") && code[j - 1] === last) return !endsOperand(lastCode(j - 2));
|
|
79
|
+
if (last === ")") return controlCloses.has(j);
|
|
80
|
+
return !"]}\"'`".includes(last);
|
|
81
|
+
};
|
|
82
|
+
/** Whether the `(` about to be emitted opens an if/while/for/with condition. */
|
|
83
|
+
const opensCondition = () => {
|
|
84
|
+
const j = lastCode(code.length - 1);
|
|
85
|
+
if (j < 0 || !isIdentifierChar(code[j])) return false;
|
|
86
|
+
let word = wordAt(j);
|
|
87
|
+
let end = j - word.length;
|
|
88
|
+
if (word === "await") {
|
|
89
|
+
end = lastCode(end);
|
|
90
|
+
word = end < 0 ? "" : wordAt(end);
|
|
91
|
+
end -= word.length;
|
|
92
|
+
}
|
|
93
|
+
return CONTROL.has(word) && code[lastCode(end)] !== ".";
|
|
94
|
+
};
|
|
95
|
+
/** Consume a regex literal at `i` if one closes on this line; `false` leaves `i` untouched. */
|
|
96
|
+
const regex = () => {
|
|
97
|
+
let j = i + 1;
|
|
98
|
+
let inClass = false;
|
|
99
|
+
for (; j < length; j++) {
|
|
100
|
+
const char = text[j] ?? "";
|
|
101
|
+
if (char === "\n" || char === "\r") return false;
|
|
102
|
+
if (char === "\\") {
|
|
103
|
+
j++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (inClass) {
|
|
107
|
+
if (char === "]") inClass = false;
|
|
108
|
+
} else if (char === "[") inClass = true;
|
|
109
|
+
else if (char === "/") break;
|
|
110
|
+
}
|
|
111
|
+
if (j >= length) return false;
|
|
112
|
+
emit("/", true, true);
|
|
113
|
+
for (let k = i + 1; k < j; k++) emit(text[k] ?? "", true, false);
|
|
114
|
+
emit("/", true, true);
|
|
115
|
+
i = j + 1;
|
|
116
|
+
while (i < length && FLAG.test(text[i] ?? "")) {
|
|
117
|
+
emit(text[i] ?? "", true, true);
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
};
|
|
122
|
+
/** Consume a `"` or `'` string starting at `i`. An unterminated one stops at the line break. */
|
|
123
|
+
const string = (quote) => {
|
|
124
|
+
const start = i;
|
|
125
|
+
emit(quote, true, true);
|
|
126
|
+
i++;
|
|
127
|
+
while (i < length) {
|
|
128
|
+
const char = text[i] ?? "";
|
|
129
|
+
if (char === "\\") {
|
|
130
|
+
emit(char, true, false);
|
|
131
|
+
i++;
|
|
132
|
+
if (i < length) {
|
|
133
|
+
emit(text[i] ?? "", true, false);
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (char === quote) {
|
|
139
|
+
emit(char, true, true);
|
|
140
|
+
i++;
|
|
141
|
+
literals.push({
|
|
142
|
+
start,
|
|
143
|
+
value: text.slice(start + 1, i - 1)
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (char === "\n") return;
|
|
148
|
+
emit(char, true, false);
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/** Consume template text up to its closing backtick or the next `${`. */
|
|
153
|
+
const templateText = () => {
|
|
154
|
+
while (i < length) {
|
|
155
|
+
const char = text[i] ?? "";
|
|
156
|
+
if (char === "\\") {
|
|
157
|
+
emit(char, true, false);
|
|
158
|
+
i++;
|
|
159
|
+
if (i < length) {
|
|
160
|
+
emit(text[i] ?? "", true, false);
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (char === "`") {
|
|
166
|
+
emit(char, true, true);
|
|
167
|
+
i++;
|
|
168
|
+
return "end";
|
|
169
|
+
}
|
|
170
|
+
if (char === "$" && text[i + 1] === "{") {
|
|
171
|
+
emit("$", true, true);
|
|
172
|
+
emit("{", true, true);
|
|
173
|
+
i += 2;
|
|
174
|
+
return "substitution";
|
|
175
|
+
}
|
|
176
|
+
emit(char, true, false);
|
|
177
|
+
i++;
|
|
178
|
+
}
|
|
179
|
+
return "eof";
|
|
180
|
+
};
|
|
181
|
+
if (text.startsWith("#!")) while (i < length && text[i] !== "\n") {
|
|
182
|
+
emit(text[i] ?? "", false, false);
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
while (i < length) {
|
|
186
|
+
const char = text[i] ?? "";
|
|
187
|
+
const next = text[i + 1];
|
|
188
|
+
if (char === "/" && next === "/") while (i < length && text[i] !== "\n") {
|
|
189
|
+
emit(text[i] ?? "", false, false);
|
|
190
|
+
i++;
|
|
191
|
+
}
|
|
192
|
+
else if (char === "/" && next === "*") {
|
|
193
|
+
const close = text.indexOf("*/", i + 2);
|
|
194
|
+
const end = close === -1 ? length : close + 2;
|
|
195
|
+
while (i < end) {
|
|
196
|
+
emit(text[i] ?? "", false, false);
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
} else if (char === "\"" || char === "'") string(char);
|
|
200
|
+
else if (char === "`") {
|
|
201
|
+
const start = i;
|
|
202
|
+
emit(char, true, true);
|
|
203
|
+
i++;
|
|
204
|
+
const stop = templateText();
|
|
205
|
+
if (stop === "end") literals.push({
|
|
206
|
+
start,
|
|
207
|
+
value: text.slice(start + 1, i - 1)
|
|
208
|
+
});
|
|
209
|
+
else if (stop === "substitution") substitutions.push(0);
|
|
210
|
+
} else if (char === "/" && regexAllowed() && regex()) {} else if (char === "(") {
|
|
211
|
+
parens.push(opensCondition());
|
|
212
|
+
emit(char, true, true);
|
|
213
|
+
i++;
|
|
214
|
+
} else if (char === ")") {
|
|
215
|
+
if (parens.pop() === true) controlCloses.add(code.length);
|
|
216
|
+
emit(char, true, true);
|
|
217
|
+
i++;
|
|
218
|
+
} else if (char === "{") {
|
|
219
|
+
const depth = substitutions.at(-1);
|
|
220
|
+
if (depth !== void 0) substitutions[substitutions.length - 1] = depth + 1;
|
|
221
|
+
emit(char, true, true);
|
|
222
|
+
i++;
|
|
223
|
+
} else if (char === "}") {
|
|
224
|
+
const depth = substitutions.at(-1);
|
|
225
|
+
emit(char, true, true);
|
|
226
|
+
i++;
|
|
227
|
+
if (depth === 0) {
|
|
228
|
+
substitutions.pop();
|
|
229
|
+
if (templateText() === "substitution") substitutions.push(0);
|
|
230
|
+
} else if (depth !== void 0) substitutions[substitutions.length - 1] = depth - 1;
|
|
231
|
+
} else {
|
|
232
|
+
emit(char, true, true);
|
|
233
|
+
i++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
withoutComments: kept.join(""),
|
|
238
|
+
code: code.join(""),
|
|
239
|
+
literals
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Whether the identifier ending just before `after` is an object-literal key or
|
|
244
|
+
* a type member (`{ process: 1 }`, `interface I { process?: string }`): the
|
|
245
|
+
* previous code character (at `before`) is `{`, `,` or `;` and the next one is
|
|
246
|
+
* `:` or `?:`. A ternary branch (`ok ? process : x`) follows `?`, so it still counts.
|
|
247
|
+
*/
|
|
248
|
+
const isPropertyKey = (code, before, after) => {
|
|
249
|
+
if (before < 0 || !"{,;".includes(code[before] ?? "")) return false;
|
|
250
|
+
let k = after;
|
|
251
|
+
while (k < code.length && SPACE.test(code[k] ?? "")) k++;
|
|
252
|
+
if (code[k] === "?") {
|
|
253
|
+
k++;
|
|
254
|
+
while (k < code.length && SPACE.test(code[k] ?? "")) k++;
|
|
255
|
+
}
|
|
256
|
+
return code[k] === ":";
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Offsets where `name` is referenced in lexed `code` as a free identifier, a
|
|
260
|
+
* member of a global object, or a spread operand. A member of any other object
|
|
261
|
+
* (`child.process`), a longer identifier, a private field and an object-literal
|
|
262
|
+
* key or type member are not.
|
|
263
|
+
*/
|
|
264
|
+
const references = (code, name) => {
|
|
265
|
+
const found = [];
|
|
266
|
+
for (let at = code.indexOf(name); at !== -1; at = code.indexOf(name, at + name.length)) {
|
|
267
|
+
const before = code[at - 1];
|
|
268
|
+
if (isIdentifierChar(before) || before === "#" || isIdentifierChar(code[at + name.length])) continue;
|
|
269
|
+
let j = at - 1;
|
|
270
|
+
while (j >= 0 && SPACE.test(code[j] ?? "")) j--;
|
|
271
|
+
if (code[j] !== ".") {
|
|
272
|
+
if (!isPropertyKey(code, j, at + name.length)) found.push(at);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (code[j - 1] === "." && code[j - 2] === ".") {
|
|
276
|
+
found.push(at);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
let end = code[j - 1] === "?" ? j - 2 : j - 1;
|
|
280
|
+
while (end >= 0 && SPACE.test(code[end] ?? "")) end--;
|
|
281
|
+
let start = end;
|
|
282
|
+
while (start >= 0 && isIdentifierChar(code[start])) start--;
|
|
283
|
+
if (GLOBAL_OBJECTS.has(code.slice(start + 1, end + 1))) found.push(at);
|
|
284
|
+
}
|
|
285
|
+
return found;
|
|
286
|
+
};
|
|
287
|
+
/** The identifier ending at or before `end` (skipping whitespace); `""` when it is itself a member access. */
|
|
288
|
+
const wordEndingAt = (code, end) => {
|
|
289
|
+
let j = end;
|
|
290
|
+
while (j >= 0 && SPACE.test(code[j] ?? "")) j--;
|
|
291
|
+
let start = j;
|
|
292
|
+
while (start >= 0 && isIdentifierChar(code[start])) start--;
|
|
293
|
+
return code[start] === "." ? "" : code.slice(start + 1, j + 1);
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* The literals that are module specifiers: after `from` or `import`, or the
|
|
297
|
+
* whole first argument of `import(` or `require(` (followed by `)` or `,`, so
|
|
298
|
+
* `import("./x" + name)` is not read as `"./x"`).
|
|
299
|
+
*/
|
|
300
|
+
const specifierLiterals = (lexed) => lexed.literals.filter((literal) => {
|
|
301
|
+
let j = literal.start - 1;
|
|
302
|
+
while (j >= 0 && SPACE.test(lexed.code[j] ?? "")) j--;
|
|
303
|
+
if (lexed.code[j] === "(") {
|
|
304
|
+
const callee = wordEndingAt(lexed.code, j - 1);
|
|
305
|
+
if (callee !== "import" && callee !== "require") return false;
|
|
306
|
+
let k = literal.start + literal.value.length + 2;
|
|
307
|
+
while (k < lexed.code.length && SPACE.test(lexed.code[k] ?? "")) k++;
|
|
308
|
+
return lexed.code[k] === ")" || lexed.code[k] === ",";
|
|
309
|
+
}
|
|
310
|
+
const keyword = wordEndingAt(lexed.code, j);
|
|
311
|
+
return keyword === "from" || keyword === "import";
|
|
312
|
+
});
|
|
313
|
+
/** A lookup from an offset in `text` to its 1-based line and UTF-16 column. */
|
|
314
|
+
const locate = (text) => {
|
|
315
|
+
const starts = [0];
|
|
316
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
|
|
317
|
+
return (offset) => {
|
|
318
|
+
let low = 0;
|
|
319
|
+
let high = starts.length - 1;
|
|
320
|
+
while (low < high) {
|
|
321
|
+
const middle = low + high + 1 >> 1;
|
|
322
|
+
if ((starts[middle] ?? 0) <= offset) low = middle;
|
|
323
|
+
else high = middle - 1;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
line: low + 1,
|
|
327
|
+
column: offset - (starts[low] ?? 0) + 1
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
//#endregion
|
|
333
|
+
export { isIdentifierChar, lex, locate, references, specifierLiterals };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/workspaces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Monorepo workspace tooling as Effect services — root discovery, package enumeration, the dependency graph, package-manager detection, pnpm catalog resolution, lockfile IO and git-based change detection.",
|
|
6
6
|
"keywords": [
|
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
"import": "./node-sync.js",
|
|
44
44
|
"default": "./node-sync.js"
|
|
45
45
|
},
|
|
46
|
+
"./testing": {
|
|
47
|
+
"types": "./testing.d.ts",
|
|
48
|
+
"import": "./testing.js",
|
|
49
|
+
"default": "./testing.js"
|
|
50
|
+
},
|
|
46
51
|
"./package.json": "./package.json"
|
|
47
52
|
},
|
|
48
53
|
"dependencies": {
|