angular-compat-matrix 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +406 -2
- package/dist/app.js +91 -0
- package/dist/build.js +167 -0
- package/dist/cleanup.js +117 -0
- package/dist/cli.js +262 -0
- package/dist/exec.js +166 -0
- package/dist/install.js +140 -0
- package/dist/library.js +200 -0
- package/dist/model.js +13 -0
- package/dist/partial.js +90 -0
- package/dist/peers.js +79 -0
- package/dist/progress.js +65 -0
- package/dist/registry.js +135 -0
- package/dist/report.js +280 -0
- package/dist/run.js +280 -0
- package/dist/scripts.js +73 -0
- package/dist/smoke.js +526 -0
- package/dist/tests.js +105 -0
- package/dist/tools.js +152 -0
- package/dist/versions.js +138 -0
- package/package.json +49 -4
package/dist/smoke.js
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** Import paths of every entry point the package's `exports` map declares. */
|
|
5
|
+
export function entryPoints(manifest) {
|
|
6
|
+
const exportsMap = manifest.exports;
|
|
7
|
+
if (!exportsMap || typeof exportsMap !== 'object')
|
|
8
|
+
return [manifest.name];
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const [key, value] of Object.entries(exportsMap)) {
|
|
11
|
+
if (!key.startsWith('.') || key.includes('*') || key === './package.json')
|
|
12
|
+
continue;
|
|
13
|
+
if (/\.(css|scss|json)$/.test(key))
|
|
14
|
+
continue;
|
|
15
|
+
if (!hasTypes(value))
|
|
16
|
+
continue;
|
|
17
|
+
entries.push(key === '.' ? manifest.name : `${manifest.name}/${key.slice(2)}`);
|
|
18
|
+
}
|
|
19
|
+
return entries.length ? entries : [manifest.name];
|
|
20
|
+
}
|
|
21
|
+
function hasTypes(value) {
|
|
22
|
+
if (typeof value === 'string')
|
|
23
|
+
return value.endsWith('.d.ts');
|
|
24
|
+
if (!value || typeof value !== 'object')
|
|
25
|
+
return false;
|
|
26
|
+
return Object.entries(value).some(([k, v]) => k === 'types' || k === 'typings' || hasTypes(v));
|
|
27
|
+
}
|
|
28
|
+
/** Reads the package's public API with the TypeScript the app itself installed. */
|
|
29
|
+
export function readPublicApi(appDir, entries) {
|
|
30
|
+
const require = createRequire(join(appDir, 'package.json'));
|
|
31
|
+
const ts = require('typescript');
|
|
32
|
+
const probe = join(appDir, 'src', '__ngm_probe.ts');
|
|
33
|
+
writeFileSync(probe, entries.map((e, i) => `import * as e${i} from ${JSON.stringify(e)};\nvoid e${i};`).join('\n'));
|
|
34
|
+
try {
|
|
35
|
+
const program = ts.createProgram([probe], {
|
|
36
|
+
target: ts.ScriptTarget.ES2022,
|
|
37
|
+
module: ts.ModuleKind.ESNext,
|
|
38
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
39
|
+
strict: true,
|
|
40
|
+
skipLibCheck: true,
|
|
41
|
+
noEmit: true,
|
|
42
|
+
types: [],
|
|
43
|
+
});
|
|
44
|
+
return new ApiReader(ts, program).read(program.getSourceFile(probe), entries);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
rmSync(probe, { force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class ApiReader {
|
|
51
|
+
ts;
|
|
52
|
+
checker;
|
|
53
|
+
byNode = new Map();
|
|
54
|
+
api = { entries: [], declarables: [], pipes: [], modules: [], services: [] };
|
|
55
|
+
nextId = 0;
|
|
56
|
+
constructor(ts, program) {
|
|
57
|
+
this.ts = ts;
|
|
58
|
+
this.checker = program.getTypeChecker();
|
|
59
|
+
}
|
|
60
|
+
read(probe, entries) {
|
|
61
|
+
const ts = this.ts;
|
|
62
|
+
const pendingModules = [];
|
|
63
|
+
const imports = probe.statements.filter(ts.isImportDeclaration);
|
|
64
|
+
imports.forEach((statement, index) => {
|
|
65
|
+
const entry = entries[index];
|
|
66
|
+
const moduleSymbol = this.checker.getSymbolAtLocation(statement.moduleSpecifier);
|
|
67
|
+
if (!moduleSymbol)
|
|
68
|
+
throw new Error(`TypeScript can't resolve ${entry} in the smoke app.`);
|
|
69
|
+
this.api.entries.push(entry);
|
|
70
|
+
for (const exported of this.checker.getExportsOfModule(moduleSymbol)) {
|
|
71
|
+
const target = exported.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(exported) : exported;
|
|
72
|
+
const node = target.declarations?.find(ts.isClassDeclaration);
|
|
73
|
+
if (!node)
|
|
74
|
+
continue;
|
|
75
|
+
const kind = this.classify(node, exported.name, entry);
|
|
76
|
+
if (kind?.module)
|
|
77
|
+
pendingModules.push({ node, info: kind.module });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
for (const { node, info } of pendingModules)
|
|
81
|
+
info.exports = this.moduleExports(node, new Set());
|
|
82
|
+
return this.api;
|
|
83
|
+
}
|
|
84
|
+
staticType(node, name) {
|
|
85
|
+
const ts = this.ts;
|
|
86
|
+
for (const member of node.members) {
|
|
87
|
+
if (!ts.isPropertyDeclaration(member) || !member.type)
|
|
88
|
+
continue;
|
|
89
|
+
if (!member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword))
|
|
90
|
+
continue;
|
|
91
|
+
if (member.name.getText() !== name)
|
|
92
|
+
continue;
|
|
93
|
+
if (ts.isTypeReferenceNode(member.type))
|
|
94
|
+
return member.type;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
classify(node, exportName, entry) {
|
|
99
|
+
const known = this.byNode.get(node);
|
|
100
|
+
if (known) {
|
|
101
|
+
this.setExportName(known, exportName, entry);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const cmp = this.staticType(node, 'ɵcmp');
|
|
105
|
+
const dir = this.staticType(node, 'ɵdir');
|
|
106
|
+
const pipe = this.staticType(node, 'ɵpipe');
|
|
107
|
+
const mod = this.staticType(node, 'ɵmod');
|
|
108
|
+
const prov = this.staticType(node, 'ɵprov');
|
|
109
|
+
const id = `d${this.nextId++}`;
|
|
110
|
+
const className = node.name?.text ?? 'Anonymous';
|
|
111
|
+
if (cmp || dir) {
|
|
112
|
+
const args = (cmp ?? dir).typeArguments ?? [];
|
|
113
|
+
this.byNode.set(node, id);
|
|
114
|
+
this.api.declarables.push({
|
|
115
|
+
id,
|
|
116
|
+
kind: cmp ? 'component' : 'directive',
|
|
117
|
+
className,
|
|
118
|
+
exportName,
|
|
119
|
+
entry,
|
|
120
|
+
selector: this.literal(args[1]),
|
|
121
|
+
standalone: this.literal(args[7]) === 'true',
|
|
122
|
+
inputs: this.inputs(node, args[3]),
|
|
123
|
+
typeParams: node.typeParameters?.length ?? 0,
|
|
124
|
+
});
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
if (pipe) {
|
|
128
|
+
const args = pipe.typeArguments ?? [];
|
|
129
|
+
this.byNode.set(node, id);
|
|
130
|
+
this.api.pipes.push({
|
|
131
|
+
id,
|
|
132
|
+
className,
|
|
133
|
+
exportName,
|
|
134
|
+
entry,
|
|
135
|
+
pipeName: this.literal(args[1]) ?? '',
|
|
136
|
+
standalone: this.literal(args[2]) === 'true',
|
|
137
|
+
extraArgs: this.pipeExtraArgs(node),
|
|
138
|
+
});
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
if (mod) {
|
|
142
|
+
this.byNode.set(node, id);
|
|
143
|
+
if (!exportName || !entry)
|
|
144
|
+
return {};
|
|
145
|
+
const info = { id, exportName, entry, exports: [] };
|
|
146
|
+
this.api.modules.push(info);
|
|
147
|
+
return { module: info };
|
|
148
|
+
}
|
|
149
|
+
if (prov && exportName && entry) {
|
|
150
|
+
this.byNode.set(node, id);
|
|
151
|
+
this.api.services.push({ exportName, entry, typeParams: node.typeParameters?.length ?? 0 });
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
setExportName(id, exportName, entry) {
|
|
156
|
+
if (!exportName || !entry)
|
|
157
|
+
return;
|
|
158
|
+
const item = this.api.declarables.find((d) => d.id === id) ?? this.api.pipes.find((p) => p.id === id);
|
|
159
|
+
if (item && !item.exportName) {
|
|
160
|
+
item.exportName = exportName;
|
|
161
|
+
item.entry = entry;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** What an NgModule exports: `static ɵmod: ɵɵNgModuleDeclaration<M, Declarations, Imports, Exports>`. */
|
|
165
|
+
moduleExports(node, seen) {
|
|
166
|
+
const ts = this.ts;
|
|
167
|
+
if (seen.has(node))
|
|
168
|
+
return [];
|
|
169
|
+
seen.add(node);
|
|
170
|
+
const exportsArg = this.staticType(node, 'ɵmod')?.typeArguments?.[3];
|
|
171
|
+
if (!exportsArg || !ts.isTupleTypeNode(exportsArg))
|
|
172
|
+
return [];
|
|
173
|
+
const ids = [];
|
|
174
|
+
for (const element of exportsArg.elements) {
|
|
175
|
+
if (!ts.isTypeQueryNode(element))
|
|
176
|
+
continue;
|
|
177
|
+
let symbol = this.checker.getSymbolAtLocation(element.exprName);
|
|
178
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias)
|
|
179
|
+
symbol = this.checker.getAliasedSymbol(symbol);
|
|
180
|
+
const target = symbol?.declarations?.find(ts.isClassDeclaration);
|
|
181
|
+
if (!target)
|
|
182
|
+
continue;
|
|
183
|
+
if (this.staticType(target, 'ɵmod')) {
|
|
184
|
+
if (!this.byNode.has(target))
|
|
185
|
+
this.classify(target, undefined, undefined);
|
|
186
|
+
ids.push(...this.moduleExports(target, seen));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!this.byNode.has(target))
|
|
190
|
+
this.classify(target, undefined, undefined);
|
|
191
|
+
const id = this.byNode.get(target);
|
|
192
|
+
if (id)
|
|
193
|
+
ids.push(id);
|
|
194
|
+
}
|
|
195
|
+
return ids;
|
|
196
|
+
}
|
|
197
|
+
literal(node) {
|
|
198
|
+
const ts = this.ts;
|
|
199
|
+
if (!node)
|
|
200
|
+
return undefined;
|
|
201
|
+
if (ts.isLiteralTypeNode(node)) {
|
|
202
|
+
const literal = node.literal;
|
|
203
|
+
if (ts.isStringLiteral(literal))
|
|
204
|
+
return literal.text;
|
|
205
|
+
if (literal.kind === ts.SyntaxKind.TrueKeyword)
|
|
206
|
+
return 'true';
|
|
207
|
+
if (literal.kind === ts.SyntaxKind.FalseKeyword)
|
|
208
|
+
return 'false';
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
/** Inputs map: `{ prop: "alias" }` before Angular 16, `{ prop: { alias; required; isSignal } }` since. */
|
|
213
|
+
inputs(node, map) {
|
|
214
|
+
const ts = this.ts;
|
|
215
|
+
if (!map || !ts.isTypeLiteralNode(map))
|
|
216
|
+
return [];
|
|
217
|
+
const inputs = [];
|
|
218
|
+
for (const member of map.members) {
|
|
219
|
+
if (!ts.isPropertySignature(member) || !member.type)
|
|
220
|
+
continue;
|
|
221
|
+
const property = ts.isStringLiteral(member.name) || ts.isIdentifier(member.name) ? member.name.text : member.name.getText();
|
|
222
|
+
let alias = property;
|
|
223
|
+
let required = false;
|
|
224
|
+
if (ts.isLiteralTypeNode(member.type)) {
|
|
225
|
+
alias = this.literal(member.type) ?? property;
|
|
226
|
+
}
|
|
227
|
+
else if (ts.isTypeLiteralNode(member.type)) {
|
|
228
|
+
for (const field of member.type.members) {
|
|
229
|
+
if (!ts.isPropertySignature(field) || !field.type)
|
|
230
|
+
continue;
|
|
231
|
+
const key = ts.isStringLiteral(field.name) || ts.isIdentifier(field.name) ? field.name.text : '';
|
|
232
|
+
if (key === 'alias')
|
|
233
|
+
alias = this.literal(field.type) ?? property;
|
|
234
|
+
if (key === 'required')
|
|
235
|
+
required = this.literal(field.type) === 'true';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
inputs.push({ property, alias, required, accessible: this.isPublicMember(node, property), bare: this.usedBare(node, property) });
|
|
239
|
+
}
|
|
240
|
+
return inputs;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Whether users write this input as a bare attribute (`<div myFlag>`): it takes a string,
|
|
244
|
+
* accepts string/boolean (e.g. booleanAttribute transform), or anything.
|
|
245
|
+
* If an input accepts strictly boolean (without accepting string or empty string), it must be bound
|
|
246
|
+
* with `[prop]="..."` because a bare attribute passes string `""`, causing TS2322 in strict templates.
|
|
247
|
+
*/
|
|
248
|
+
usedBare(node, property) {
|
|
249
|
+
const ts = this.ts;
|
|
250
|
+
const write = this.inputWriteType(node, property);
|
|
251
|
+
if (!write)
|
|
252
|
+
return false;
|
|
253
|
+
const parts = write.isUnion() ? write.types : [write];
|
|
254
|
+
// Accepts string, any, unknown, or empty string literal "" (from booleanAttribute or union)
|
|
255
|
+
const stringFlags = ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.String;
|
|
256
|
+
return parts.some((part) => (part.flags & stringFlags) !== 0 || (part.isStringLiteral() && part.value === ''));
|
|
257
|
+
}
|
|
258
|
+
/** The type a binding to the input must have: a signal input's write type, `ngAcceptInputType_*`, or the property's type. */
|
|
259
|
+
inputWriteType(node, property) {
|
|
260
|
+
const ts = this.ts;
|
|
261
|
+
if (!node.name)
|
|
262
|
+
return undefined;
|
|
263
|
+
const classSymbol = this.checker.getSymbolAtLocation(node.name);
|
|
264
|
+
if (!classSymbol)
|
|
265
|
+
return undefined;
|
|
266
|
+
const accept = this.checker.getTypeOfSymbolAtLocation(classSymbol, node).getProperty(`ngAcceptInputType_${property}`);
|
|
267
|
+
if (accept)
|
|
268
|
+
return this.checker.getTypeOfSymbolAtLocation(accept, node);
|
|
269
|
+
const setter = this.setter(node, property);
|
|
270
|
+
if (setter?.parameters[0])
|
|
271
|
+
return this.checker.getTypeAtLocation(setter.parameters[0]);
|
|
272
|
+
const member = this.checker.getDeclaredTypeOfSymbol(classSymbol).getProperty(property);
|
|
273
|
+
if (!member)
|
|
274
|
+
return undefined;
|
|
275
|
+
const type = this.checker.getTypeOfSymbolAtLocation(member, node);
|
|
276
|
+
const name = type.getSymbol()?.getName() ?? type.aliasSymbol?.getName();
|
|
277
|
+
const isReference = (type.flags & ts.TypeFlags.Object) !== 0 && (type.objectFlags & ts.ObjectFlags.Reference) !== 0;
|
|
278
|
+
const args = isReference ? this.checker.getTypeArguments(type) : (type.aliasTypeArguments ?? []);
|
|
279
|
+
if (name === 'InputSignalWithTransform')
|
|
280
|
+
return args[1];
|
|
281
|
+
if (name === 'InputSignal' || name === 'ModelSignal')
|
|
282
|
+
return args[0];
|
|
283
|
+
return type;
|
|
284
|
+
}
|
|
285
|
+
/** An input written through a setter, whose type can differ from what the getter returns. */
|
|
286
|
+
setter(node, name) {
|
|
287
|
+
const ts = this.ts;
|
|
288
|
+
return node.members.find((m) => ts.isSetAccessorDeclaration(m) && (ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) && m.name.text === name);
|
|
289
|
+
}
|
|
290
|
+
isPublicMember(node, name) {
|
|
291
|
+
const ts = this.ts;
|
|
292
|
+
// `Class['prop']` is the getter's type; a setter's type can't be named, so those get `any`.
|
|
293
|
+
if (this.setter(node, name))
|
|
294
|
+
return false;
|
|
295
|
+
const member = node.members.find((m) => m.name && (ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) && m.name.text === name);
|
|
296
|
+
if (!member)
|
|
297
|
+
return false; // inherited or accessor-only: don't guess its type
|
|
298
|
+
const modifiers = ts.canHaveModifiers(member) ? (ts.getModifiers(member) ?? []) : [];
|
|
299
|
+
return !modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
300
|
+
}
|
|
301
|
+
pipeExtraArgs(node) {
|
|
302
|
+
const ts = this.ts;
|
|
303
|
+
const transforms = node.members.filter((m) => ts.isMethodDeclaration(m) && m.name.getText() === 'transform');
|
|
304
|
+
if (!transforms.length)
|
|
305
|
+
return 0;
|
|
306
|
+
const required = transforms.map((method) => method.parameters.filter((p) => !p.questionToken && !p.initializer && !p.dotDotDotToken).length);
|
|
307
|
+
return Math.max(0, Math.min(...required) - 1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** The first usable alternative of a selector like `button[mat-button], a[mat-button]`. */
|
|
311
|
+
export function parseSelector(selector) {
|
|
312
|
+
for (const raw of splitTopLevel(selector)) {
|
|
313
|
+
const part = raw.replace(/:not\([^)]*\)/g, '').trim();
|
|
314
|
+
if (!part)
|
|
315
|
+
continue;
|
|
316
|
+
const tag = /^[a-zA-Z][\w-]*/.exec(part)?.[0];
|
|
317
|
+
const attributes = [...part.matchAll(/\[([^\]=~|^$*\s]+)\s*(?:[~|^$*]?=\s*["']?([^"'\]]*)["']?)?\]/g)].map((m) => ({
|
|
318
|
+
name: m[1],
|
|
319
|
+
value: m[2],
|
|
320
|
+
}));
|
|
321
|
+
const classes = [...part.replace(/\[[^\]]*\]/g, '').matchAll(/\.([\w-]+)/g)].map((m) => m[1]);
|
|
322
|
+
if (!tag && !attributes.length && !classes.length)
|
|
323
|
+
continue;
|
|
324
|
+
return { tag, attributes, classes };
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
function splitTopLevel(selector) {
|
|
329
|
+
const parts = [];
|
|
330
|
+
let depth = 0;
|
|
331
|
+
let current = '';
|
|
332
|
+
for (const char of selector) {
|
|
333
|
+
if (char === '(' || char === '[')
|
|
334
|
+
depth++;
|
|
335
|
+
if (char === ')' || char === ']')
|
|
336
|
+
depth--;
|
|
337
|
+
if (char === ',' && depth === 0) {
|
|
338
|
+
parts.push(current);
|
|
339
|
+
current = '';
|
|
340
|
+
}
|
|
341
|
+
else
|
|
342
|
+
current += char;
|
|
343
|
+
}
|
|
344
|
+
parts.push(current);
|
|
345
|
+
return parts;
|
|
346
|
+
}
|
|
347
|
+
const VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
|
|
348
|
+
export function renderSmoke(api, options) {
|
|
349
|
+
const names = new Map(); // `${entry}\u0000${export}` -> local identifier
|
|
350
|
+
const used = new Set(['Component', 'CUSTOM_ELEMENTS_SCHEMA', 'inject', 'NgmSmoke', 'NgmWrite']);
|
|
351
|
+
const local = (entry, exportName) => {
|
|
352
|
+
const key = `${entry}\u0000${exportName}`;
|
|
353
|
+
let name = names.get(key);
|
|
354
|
+
if (!name) {
|
|
355
|
+
name = exportName;
|
|
356
|
+
for (let n = 2; used.has(name); n++)
|
|
357
|
+
name = `${exportName}_${n}`;
|
|
358
|
+
used.add(name);
|
|
359
|
+
names.set(key, name);
|
|
360
|
+
}
|
|
361
|
+
return name;
|
|
362
|
+
};
|
|
363
|
+
const uses = [];
|
|
364
|
+
const counts = { components: 0, directives: 0, pipes: 0, modules: 0, services: 0 };
|
|
365
|
+
let field = 0;
|
|
366
|
+
const element = (declarable, fields) => {
|
|
367
|
+
const selector = declarable.selector ? parseSelector(declarable.selector) : undefined;
|
|
368
|
+
if (!selector)
|
|
369
|
+
return undefined;
|
|
370
|
+
const selectorAttributes = new Set(selector.attributes.map((a) => a.name));
|
|
371
|
+
// Required inputs get a value; so does a selector attribute that is a number or object input,
|
|
372
|
+
// since users bind those rather than writing them bare.
|
|
373
|
+
const bound = declarable.inputs.filter((i) => i.required || (selectorAttributes.has(i.alias) && !i.bare));
|
|
374
|
+
const boundAliases = new Set(bound.map((i) => i.alias));
|
|
375
|
+
const attrs = [];
|
|
376
|
+
for (const attribute of selector.attributes) {
|
|
377
|
+
if (boundAliases.has(attribute.name))
|
|
378
|
+
continue;
|
|
379
|
+
attrs.push(attribute.value === undefined ? attribute.name : `${attribute.name}="${escapeAttr(attribute.value)}"`);
|
|
380
|
+
}
|
|
381
|
+
if (selector.classes.length)
|
|
382
|
+
attrs.push(`class="${selector.classes.join(' ')}"`);
|
|
383
|
+
for (const input of bound) {
|
|
384
|
+
const name = `v${field++}`;
|
|
385
|
+
const type = declarable.exportName && declarable.entry && input.accessible
|
|
386
|
+
? writeType(local(declarable.entry, declarable.exportName), declarable.typeParams, input.property)
|
|
387
|
+
: 'any';
|
|
388
|
+
fields.push(` protected readonly ${name} = undefined as unknown as ${type};`);
|
|
389
|
+
attrs.push(`[${input.alias}]="${name}"`);
|
|
390
|
+
}
|
|
391
|
+
const tag = selector.tag ?? 'div';
|
|
392
|
+
const open = `<${tag}${attrs.length ? ' ' + attrs.join(' ') : ''}>`;
|
|
393
|
+
counts[declarable.kind === 'component' ? 'components' : 'directives']++;
|
|
394
|
+
return VOID_ELEMENTS.has(tag.toLowerCase()) ? open : `${open}</${tag}>`;
|
|
395
|
+
};
|
|
396
|
+
const pipeUse = (pipe, fields) => {
|
|
397
|
+
if (!fields.some((f) => f.includes('pipeValue'))) {
|
|
398
|
+
fields.push(' protected readonly pipeValue: any = undefined;', ' protected readonly pipeArg: any = undefined;');
|
|
399
|
+
}
|
|
400
|
+
counts.pipes++;
|
|
401
|
+
const args = Array.from({ length: pipe.extraArgs }, () => ' : pipeArg').join('');
|
|
402
|
+
return `<span>{{ pipeValue | ${pipe.pipeName}${args} }}</span>`;
|
|
403
|
+
};
|
|
404
|
+
// An NgModule, with everything it exports used in one template, as its users would.
|
|
405
|
+
for (const module of api.modules) {
|
|
406
|
+
const use = { imports: [local(module.entry, module.exportName)], template: [], fields: [] };
|
|
407
|
+
counts.modules++;
|
|
408
|
+
for (const id of module.exports) {
|
|
409
|
+
const declarable = api.declarables.find((d) => d.id === id);
|
|
410
|
+
const pipe = api.pipes.find((p) => p.id === id);
|
|
411
|
+
const line = declarable ? element(declarable, use.fields) : pipe?.pipeName ? pipeUse(pipe, use.fields) : undefined;
|
|
412
|
+
if (line)
|
|
413
|
+
use.template.push(line);
|
|
414
|
+
}
|
|
415
|
+
uses.push(use);
|
|
416
|
+
}
|
|
417
|
+
// Standalone declarables and pipes, one at a time. Only those with a selector are imported: an
|
|
418
|
+
// abstract base directive can't go in `imports`, and an unused import is a warning.
|
|
419
|
+
const importable = (item) => item.standalone && Boolean(item.exportName && item.entry);
|
|
420
|
+
for (const declarable of api.declarables) {
|
|
421
|
+
if (!importable(declarable))
|
|
422
|
+
continue;
|
|
423
|
+
const use = { imports: [], template: [], fields: [] };
|
|
424
|
+
const line = element(declarable, use.fields);
|
|
425
|
+
if (!line)
|
|
426
|
+
continue;
|
|
427
|
+
use.imports.push(local(declarable.entry, declarable.exportName));
|
|
428
|
+
use.template.push(line);
|
|
429
|
+
uses.push(use);
|
|
430
|
+
}
|
|
431
|
+
for (const pipe of api.pipes) {
|
|
432
|
+
if (!importable(pipe) || !pipe.pipeName)
|
|
433
|
+
continue;
|
|
434
|
+
const use = { imports: [local(pipe.entry, pipe.exportName)], template: [], fields: [] };
|
|
435
|
+
use.template.push(pipeUse(pipe, use.fields));
|
|
436
|
+
uses.push(use);
|
|
437
|
+
}
|
|
438
|
+
const rootFields = [];
|
|
439
|
+
for (const service of api.services) {
|
|
440
|
+
const name = local(service.entry, service.exportName);
|
|
441
|
+
const type = service.typeParams ? `${name}<${Array(service.typeParams).fill('any').join(', ')}>` : name;
|
|
442
|
+
rootFields.push(` protected readonly s${field++} = inject<${type}>(${name}, { optional: true });`);
|
|
443
|
+
counts.services++;
|
|
444
|
+
}
|
|
445
|
+
// Namespace imports pull every export into the bundle, so the linker sees every declaration.
|
|
446
|
+
const namespaces = api.entries.map((entry, i) => `import * as ngmEntry${i} from ${JSON.stringify(entry)};`);
|
|
447
|
+
rootFields.unshift(` protected readonly ngmEntries = [${api.entries.map((_, i) => `ngmEntry${i}`).join(', ')}];`);
|
|
448
|
+
const named = new Map();
|
|
449
|
+
for (const [key, localName] of names) {
|
|
450
|
+
const [entry, exportName] = key.split('\u0000');
|
|
451
|
+
const list = named.get(entry) ?? [];
|
|
452
|
+
list.push(exportName === localName ? exportName : `${exportName} as ${localName}`);
|
|
453
|
+
named.set(entry, list);
|
|
454
|
+
}
|
|
455
|
+
const namedImports = [...named].map(([entry, list]) => `import { ${list.sort().join(', ')} } from ${JSON.stringify(entry)};`);
|
|
456
|
+
const coreImports = ['Component', 'CUSTOM_ELEMENTS_SCHEMA', 'inject'];
|
|
457
|
+
if (options.signalWriteBrand)
|
|
458
|
+
coreImports.push('ɵINPUT_SIGNAL_BRAND_WRITE_TYPE');
|
|
459
|
+
const writeHelper = options.signalWriteBrand
|
|
460
|
+
? `type NgmWrite<S, I, K extends keyof I> = I[K] extends { [ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: infer W }
|
|
461
|
+
? W
|
|
462
|
+
: S extends { [P in \`ngAcceptInputType_\${K & string}\`]: infer A }
|
|
463
|
+
? A
|
|
464
|
+
: I[K];`
|
|
465
|
+
: `type NgmWrite<S, I, K extends keyof I> = S extends { [P in \`ngAcceptInputType_\${K & string}\`]: infer A } ? A : I[K];`;
|
|
466
|
+
const component = (name, selector, imports, template, fields) => `@Component({
|
|
467
|
+
selector: '${selector}',
|
|
468
|
+
standalone: true,
|
|
469
|
+
imports: [${imports.join(', ')}],
|
|
470
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
471
|
+
template: \`
|
|
472
|
+
${template.map((line) => ' ' + escapeTemplate(line)).join('\n')}
|
|
473
|
+
\`,
|
|
474
|
+
})
|
|
475
|
+
${name === 'NgmSmoke' ? 'export ' : ''}class ${name} {
|
|
476
|
+
${fields.join('\n')}
|
|
477
|
+
}
|
|
478
|
+
`;
|
|
479
|
+
const useNames = uses.map((_, i) => `NgmUse${i}`);
|
|
480
|
+
const source = `// Generated by ng-matrix: uses every public export of the package once.
|
|
481
|
+
import { ${coreImports.join(', ')} } from '@angular/core';
|
|
482
|
+
${namespaces.join('\n')}
|
|
483
|
+
${namedImports.join('\n')}
|
|
484
|
+
|
|
485
|
+
/** The type a template binding to this input must have. */
|
|
486
|
+
${writeHelper}
|
|
487
|
+
|
|
488
|
+
${uses.map((use, i) => component(useNames[i], `ngm-use-${i}`, use.imports, use.template, use.fields)).join('\n')}
|
|
489
|
+
${component('NgmSmoke', 'app-root', useNames, useNames.map((_, i) => `<ngm-use-${i}></ngm-use-${i}>`), rootFields)}`;
|
|
490
|
+
const main = `// Generated by ng-matrix.
|
|
491
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
492
|
+
import { NgmSmoke } from './ngm-smoke';
|
|
493
|
+
|
|
494
|
+
bootstrapApplication(NgmSmoke).catch((error) => console.error(error));
|
|
495
|
+
`;
|
|
496
|
+
return { component: source, main, counts };
|
|
497
|
+
}
|
|
498
|
+
function writeType(className, typeParams, property) {
|
|
499
|
+
const instance = typeParams ? `${className}<${Array(typeParams).fill('any').join(', ')}>` : className;
|
|
500
|
+
return `NgmWrite<typeof ${className}, ${instance}, ${JSON.stringify(property)}>`;
|
|
501
|
+
}
|
|
502
|
+
function escapeAttr(value) {
|
|
503
|
+
return value.replace(/"/g, '"');
|
|
504
|
+
}
|
|
505
|
+
function escapeTemplate(line) {
|
|
506
|
+
return line.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
507
|
+
}
|
|
508
|
+
export function hasSignalWriteBrand(appDir) {
|
|
509
|
+
const require = createRequire(join(appDir, 'package.json'));
|
|
510
|
+
try {
|
|
511
|
+
const manifest = require.resolve('@angular/core/package.json');
|
|
512
|
+
const version = JSON.parse(readFileSync(manifest, 'utf8')).version;
|
|
513
|
+
const [major, minor] = version.split('.').map(Number);
|
|
514
|
+
return major > 17 || (major === 17 && minor >= 1);
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
export function writeSmoke(appDir, files) {
|
|
521
|
+
writeFileSync(join(appDir, 'src', 'ngm-smoke.ts'), files.component);
|
|
522
|
+
writeFileSync(join(appDir, 'src', 'ngm-main.ts'), files.main);
|
|
523
|
+
}
|
|
524
|
+
export function smokeExists(appDir) {
|
|
525
|
+
return existsSync(join(appDir, 'src', 'ngm-smoke.ts'));
|
|
526
|
+
}
|
package/dist/tests.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { cpSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
import { readJson, writeJson } from "./app.js";
|
|
5
|
+
import { environmentProblem, errorLines, exec, npm, quietEnv, stepTimeout } from "./exec.js";
|
|
6
|
+
import { FRAMEWORK } from "./peers.js";
|
|
7
|
+
import { npmInstall, pnpmBuildSettings } from "./scripts.js";
|
|
8
|
+
import { ensureTool, PNPM_VERSION, pnpmEntry, pnpmShimDir, withPath } from "./tools.js";
|
|
9
|
+
/** Packages released with the Angular CLI, versioned with it. */
|
|
10
|
+
const TOOLING = /^(@angular\/(cli|build|ssr|pwa)|@angular-devkit\/.+|@schematics\/angular)$/;
|
|
11
|
+
const SKIP = new Set(['node_modules', '.git', '.angular', 'dist', '.nx', '.ng-matrix', 'coverage']);
|
|
12
|
+
/**
|
|
13
|
+
* The version every Angular-related dependency of the workspace gets for this major:
|
|
14
|
+
* framework packages match @angular/core, tooling matches the CLI, and TypeScript, zone.js,
|
|
15
|
+
* RxJS and ng-packagr get the ranges this Angular accepts, for the package manager to resolve.
|
|
16
|
+
*/
|
|
17
|
+
export function swapVersions(deps, request) {
|
|
18
|
+
const { angular, peerVersions } = request;
|
|
19
|
+
const out = { ...deps };
|
|
20
|
+
for (const name of Object.keys(deps)) {
|
|
21
|
+
if (FRAMEWORK.has(name))
|
|
22
|
+
out[name] = angular.version;
|
|
23
|
+
else if (TOOLING.test(name))
|
|
24
|
+
out[name] = angular.cliVersion;
|
|
25
|
+
else if (peerVersions[name])
|
|
26
|
+
out[name] = peerVersions[name];
|
|
27
|
+
else if (name === 'ng-packagr')
|
|
28
|
+
out[name] = angular.prerelease ? `>=${angular.major}.0.0-0` : `^${angular.major}.0.0`;
|
|
29
|
+
else if (name === 'typescript' && angular.typescriptRange)
|
|
30
|
+
out[name] = angular.typescriptRange;
|
|
31
|
+
else if (name === 'zone.js' && angular.zoneRange)
|
|
32
|
+
out[name] = angular.zoneRange;
|
|
33
|
+
else if (name === 'rxjs' && angular.rxjsRange) {
|
|
34
|
+
const current = semver.minVersion(deps[name]);
|
|
35
|
+
if (!current || !semver.satisfies(current, angular.rxjsRange))
|
|
36
|
+
out[name] = angular.rxjsRange;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** Runs the library's own tests in a copy of its workspace, with Angular swapped to this version. */
|
|
42
|
+
export async function runLibraryTests(request) {
|
|
43
|
+
const started = Date.now();
|
|
44
|
+
rmSync(request.into, { recursive: true, force: true });
|
|
45
|
+
cpSync(request.workspace, request.into, {
|
|
46
|
+
recursive: true,
|
|
47
|
+
filter: (source) => {
|
|
48
|
+
const rel = relative(request.workspace, source);
|
|
49
|
+
return !rel || !rel.split(sep).some((part) => SKIP.has(part));
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
for (const lock of ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'])
|
|
53
|
+
rmSync(join(request.into, lock), { force: true });
|
|
54
|
+
const manifestPath = join(request.into, 'package.json');
|
|
55
|
+
const manifest = readJson(manifestPath);
|
|
56
|
+
if (manifest.dependencies)
|
|
57
|
+
manifest.dependencies = swapVersions(manifest.dependencies, request);
|
|
58
|
+
if (manifest.devDependencies)
|
|
59
|
+
manifest.devDependencies = swapVersions(manifest.devDependencies, request);
|
|
60
|
+
if (request.pm === 'pnpm')
|
|
61
|
+
manifest.packageManager = `pnpm@${PNPM_VERSION}`;
|
|
62
|
+
else
|
|
63
|
+
delete manifest.packageManager;
|
|
64
|
+
writeJson(manifestPath, manifest);
|
|
65
|
+
let env = quietEnv({ CI: '1' });
|
|
66
|
+
let install;
|
|
67
|
+
if (request.pm === 'npm') {
|
|
68
|
+
const args = ['install', '--legacy-peer-deps', '--no-audit', '--no-fund'];
|
|
69
|
+
if (request.offline)
|
|
70
|
+
args.push('--offline');
|
|
71
|
+
install = await npmInstall(args, { cwd: request.into, env, timeoutMs: stepTimeout() }, request.allowScripts);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
writeFileSync(join(request.into, 'pnpm-workspace.yaml'), ['strictPeerDependencies: false', ...pnpmBuildSettings(request.allowScripts)].join('\n') + '\n');
|
|
75
|
+
const pnpm = await ensureTool(request.cacheDir, 'pnpm', PNPM_VERSION, request.offline);
|
|
76
|
+
// The test command runs in a shell; it has to find this pnpm, not whatever is (or isn't) installed.
|
|
77
|
+
env = withPath(env, pnpmShimDir(request.cacheDir, pnpm));
|
|
78
|
+
install = await exec(process.execPath, [pnpmEntry(pnpm), 'install', '--no-frozen-lockfile'], {
|
|
79
|
+
cwd: request.into,
|
|
80
|
+
env,
|
|
81
|
+
timeoutMs: stepTimeout(),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (install.code !== 0) {
|
|
85
|
+
return {
|
|
86
|
+
pass: false,
|
|
87
|
+
message: `Installing the workspace with Angular ${request.angular.version} failed.`,
|
|
88
|
+
errorLines: errorLines(install.output),
|
|
89
|
+
fullOutput: install.output,
|
|
90
|
+
durationMs: Date.now() - started,
|
|
91
|
+
environment: environmentProblem(install),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const command = request.command ?? `${request.pm} test`;
|
|
95
|
+
const result = await exec(command, [], { cwd: request.into, env, shell: true, timeoutMs: stepTimeout() });
|
|
96
|
+
const tail = result.output.split(/\r?\n/).filter((l) => l.trim()).slice(-8);
|
|
97
|
+
return {
|
|
98
|
+
pass: result.code === 0,
|
|
99
|
+
message: result.code === 0 ? `\`${command}\` passed.` : `\`${command}\` exited with ${result.code}.`,
|
|
100
|
+
errorLines: result.code === 0 ? tail.slice(-3) : errorLines(result.output),
|
|
101
|
+
fullOutput: result.output,
|
|
102
|
+
durationMs: Date.now() - started,
|
|
103
|
+
environment: result.code === 0 ? undefined : environmentProblem(result),
|
|
104
|
+
};
|
|
105
|
+
}
|