@transclude/core 0.2.0 → 0.3.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/README.md +22 -3
- package/bin/build.js +9 -3
- package/bin/check.js +30 -8
- package/bin/dev.js +10 -2
- package/package.json +3 -2
- package/skills/transclude/SKILL.md +1 -1
- package/src/address.js +9 -2
- package/src/app.js +110 -81
- package/src/compiler/ambient.js +99 -0
- package/src/compiler/bind.js +34 -27
- package/src/compiler/codegen.js +40 -41
- package/src/compiler/directives.js +29 -0
- package/src/compiler/expr.js +10 -1
- package/src/compiler/html.js +41 -0
- package/src/compiler/index.js +46 -8
- package/src/compiler/script.js +10 -3
- package/src/compiler/shim.js +14 -41
- package/src/compiler/types.js +32 -5
- package/src/csp.js +7 -1
- package/src/document.js +63 -12
- package/src/extract.js +9 -8
- package/src/plugin.js +28 -7
- package/src/precache.js +11 -1
- package/src/proxy.js +9 -1
- package/src/rewrite.js +12 -5
- package/src/runtime/index.js +8 -5
- package/src/sitemap.js +3 -3
- package/src/static-cache.js +4 -1
- package/src/typecheck.js +104 -10
package/src/typecheck.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import ts from 'typescript';
|
|
18
|
+
import { AMBIENT_NAMES } from './compiler/ambient.js';
|
|
18
19
|
import { buildEndpointShim, buildShim, originalOffset } from './compiler/shim.js';
|
|
19
20
|
import { splitBlocks, readFlags } from './compiler/index.js';
|
|
20
21
|
import { resolveRoutesDir, scanRoutes } from './routes.js';
|
|
@@ -45,14 +46,38 @@ const compilerOptions = (strict) => ({
|
|
|
45
46
|
types: [],
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
// `UseFullyQualifiedType` is what makes a name the app declared resolvable
|
|
50
|
+
// somewhere else. Without it a `@typedef {…} Post` in the app prints as `Post`,
|
|
51
|
+
// which means something in the file it came from and nothing in
|
|
52
|
+
// transclude-env.d.ts, where it landed as an undeclared name.
|
|
48
53
|
const TYPE_FORMAT =
|
|
49
54
|
ts.TypeFormatFlags.NoTruncation |
|
|
50
55
|
ts.TypeFormatFlags.InTypeAlias |
|
|
56
|
+
ts.TypeFormatFlags.UseFullyQualifiedType |
|
|
51
57
|
ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType;
|
|
52
58
|
|
|
53
59
|
const LAYOUT_FILE = '_layout.html';
|
|
54
60
|
|
|
55
61
|
/**
|
|
62
|
+
* The checker, and everything it needs held in one closure.
|
|
63
|
+
*
|
|
64
|
+
* This is long and stays long: every helper below reads the language service,
|
|
65
|
+
* the shim map or the resolved project, and handing each of them five arguments
|
|
66
|
+
* instead would be more to read rather than less. It is a list of small named
|
|
67
|
+
* functions, in this order:
|
|
68
|
+
*
|
|
69
|
+
* the language service host, install, sourceOf
|
|
70
|
+
* reading a type back exportTypeOf, expand, resolveNames, and the four
|
|
71
|
+
* `…TypeOf` shorthands
|
|
72
|
+
* finding the project elementFiles, layoutFiles, ancestorsOf, chainFor
|
|
73
|
+
* what a shim is given contextLiteral, endpointLiteral, mergeTypes
|
|
74
|
+
* building them build, refresh
|
|
75
|
+
* what callers use the returned object
|
|
76
|
+
*
|
|
77
|
+
* `build` is the one to read first. It compiles every shim in dependency order,
|
|
78
|
+
* which is the only order that resolves: an element depends on nothing, a layout
|
|
79
|
+
* on the layouts above it, a page on its whole chain.
|
|
80
|
+
*
|
|
56
81
|
* @param {{ root: string, appDir: string, routesDir: string, elementsDir: string,
|
|
57
82
|
* strict?: boolean }} options
|
|
58
83
|
* @returns {{ files: Function, update: Function, rebuild: Function,
|
|
@@ -121,6 +146,53 @@ export function createChecker({
|
|
|
121
146
|
return text === 'any' ? 'unknown' : text;
|
|
122
147
|
};
|
|
123
148
|
|
|
149
|
+
// `UseFullyQualifiedType` prints a named type as `import("/abs/file").Name`.
|
|
150
|
+
// Inside a shim that resolves and is what keeps a prop structurally checked.
|
|
151
|
+
// In transclude-env.d.ts it does not: a shim path is `<file>.js` for an .html
|
|
152
|
+
// file nobody can import, and an absolute path would name this machine.
|
|
153
|
+
const QUALIFIED = /import\("([^"]+)"\)\.([A-Za-z_$][\w$]*)/g;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The type a name stands for, expanded. `InTypeAlias` is what stops tsc
|
|
157
|
+
* printing the alias for the type it was asked to print, so this returns the
|
|
158
|
+
* shape rather than the name again. A type naming itself terminates: the
|
|
159
|
+
* placeholder is in place before the expansion is resolved.
|
|
160
|
+
*/
|
|
161
|
+
const expand = (file, name, into) => {
|
|
162
|
+
const key = `${file}\0${name}`;
|
|
163
|
+
const already = into.byKey.get(key);
|
|
164
|
+
if (already) return already;
|
|
165
|
+
|
|
166
|
+
const program = service.getProgram();
|
|
167
|
+
// tsc prints the path with no extension, and a shim is the source it names
|
|
168
|
+
// plus `.js`.
|
|
169
|
+
const source = program?.getSourceFile(file) ?? program?.getSourceFile(`${file}.js`);
|
|
170
|
+
const checker = program?.getTypeChecker();
|
|
171
|
+
const moduleSymbol = source && checker?.getSymbolAtLocation(source);
|
|
172
|
+
const symbol =
|
|
173
|
+
moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((s) => s.getName() === name);
|
|
174
|
+
// Two files can each declare a `Post`, and one name cannot mean both.
|
|
175
|
+
let display = name;
|
|
176
|
+
for (let n = 2; into.text.has(display); n++) display = `${name}_${n}`;
|
|
177
|
+
if (!symbol) return display;
|
|
178
|
+
|
|
179
|
+
into.byKey.set(key, display);
|
|
180
|
+
into.text.set(display, '');
|
|
181
|
+
const declared = checker.getDeclaredTypeOfSymbol(symbol);
|
|
182
|
+
into.text.set(display, resolveNames(checker.typeToString(declared, undefined, TYPE_FORMAT), into));
|
|
183
|
+
return display;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Every qualified name in a type string, replaced by a bare one. What the
|
|
188
|
+
* compiler declares for itself is left to the emitted file, which writes the
|
|
189
|
+
* same shapes from `ambient.js`; anything else is the app's and is expanded.
|
|
190
|
+
*/
|
|
191
|
+
const resolveNames = (type, into) =>
|
|
192
|
+
type.replace(QUALIFIED, (_, file, name) =>
|
|
193
|
+
AMBIENT_NAMES.has(name) ? name : expand(file, name, into),
|
|
194
|
+
);
|
|
195
|
+
|
|
124
196
|
const dataTypeOf = (file) => exportTypeOf(file, '__data');
|
|
125
197
|
const propTypeOf = (file) => exportTypeOf(file, '__propTypes');
|
|
126
198
|
const memberTypeOf = (file) => exportTypeOf(file, '__members');
|
|
@@ -426,8 +498,18 @@ export function createChecker({
|
|
|
426
498
|
};
|
|
427
499
|
},
|
|
428
500
|
|
|
429
|
-
/**
|
|
501
|
+
/**
|
|
502
|
+
* Everything transclude-env.d.ts is written from.
|
|
503
|
+
*
|
|
504
|
+
* Every type string is passed through `named`, which turns a qualified
|
|
505
|
+
* reference into a bare name and collects what that name is. A type the app
|
|
506
|
+
* declared is reachable from the file it was written in and from nowhere
|
|
507
|
+
* else, so the emitted file carries its own copy rather than an import: one
|
|
508
|
+
* declared in an .html file has no module to be imported from at all.
|
|
509
|
+
*/
|
|
430
510
|
describe() {
|
|
511
|
+
const aliases = { byKey: new Map(), text: new Map() };
|
|
512
|
+
const named = (type) => resolveNames(type, aliases);
|
|
431
513
|
// Light elements are described separately: they have props but no shadow
|
|
432
514
|
// root, so nothing about `this.shadowRoot` applies to them.
|
|
433
515
|
const partialTags = new Set(
|
|
@@ -435,26 +517,38 @@ export function createChecker({
|
|
|
435
517
|
.filter((file) => !isShadow(file))
|
|
436
518
|
.map((file) => path.basename(file, '.html')),
|
|
437
519
|
);
|
|
438
|
-
|
|
520
|
+
const element = ([tag, type]) => {
|
|
521
|
+
const { members, state, upgrades } = project.componentMembers.get(tag) ?? {};
|
|
522
|
+
return {
|
|
523
|
+
tag,
|
|
524
|
+
type: named(type),
|
|
525
|
+
upgrades,
|
|
526
|
+
members: members ? named(members) : members,
|
|
527
|
+
state: state ? named(state) : state,
|
|
528
|
+
};
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const described = {
|
|
439
532
|
components: [...project.componentProps]
|
|
440
533
|
.filter(([tag]) => !partialTags.has(tag))
|
|
441
|
-
.map(
|
|
442
|
-
partials: [...project.componentProps]
|
|
443
|
-
.filter(([tag]) => partialTags.has(tag))
|
|
444
|
-
.map(([tag, type]) => ({ tag, type, ...project.componentMembers.get(tag) })),
|
|
534
|
+
.map(element),
|
|
535
|
+
partials: [...project.componentProps].filter(([tag]) => partialTags.has(tag)).map(element),
|
|
445
536
|
layouts: [...project.layoutData].map(([id, type]) => ({
|
|
446
537
|
id,
|
|
447
|
-
type,
|
|
448
|
-
context: contextFor(project.layouts.get(id)),
|
|
538
|
+
type: named(type),
|
|
539
|
+
context: named(contextFor(project.layouts.get(id))),
|
|
449
540
|
})),
|
|
450
541
|
pages: [...project.pages].map(([id, { route, context }]) => ({
|
|
451
542
|
id,
|
|
452
543
|
params: route.params,
|
|
453
544
|
pattern: route.pattern,
|
|
454
|
-
context,
|
|
455
|
-
type: dataTypeOf(route.file),
|
|
545
|
+
context: named(context),
|
|
546
|
+
type: named(dataTypeOf(route.file)),
|
|
456
547
|
})),
|
|
457
548
|
};
|
|
549
|
+
|
|
550
|
+
// Collected while the above was named, so it is read after, not during.
|
|
551
|
+
return { ...described, types: [...aliases.text].map(([name, type]) => ({ name, type })) };
|
|
458
552
|
},
|
|
459
553
|
};
|
|
460
554
|
}
|