@transclude/core 0.1.1 → 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/LICENSE +1 -1
- package/README.md +65 -14
- package/bin/build.js +22 -4
- package/bin/check.js +30 -8
- package/bin/dev.js +10 -2
- package/bin/release.js +19 -4
- package/package.json +13 -4
- package/skills/transclude/SKILL.md +219 -0
- package/skills/transclude/references/elements.md +206 -0
- package/skills/transclude/references/fragments.md +168 -0
- package/skills/transclude/references/server.md +155 -0
- 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 +59 -35
- 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 +63 -11
- package/src/compiler/script.js +40 -11
- package/src/compiler/shim.js +24 -45
- 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/negotiate.js +1 -1
- package/src/plugin.js +28 -7
- package/src/precache.js +11 -1
- package/src/project.js +29 -1
- package/src/proxy.js +9 -1
- package/src/rewrite.js +12 -5
- package/src/routes.js +1 -1
- package/src/runtime/index.js +12 -9
- package/src/server.js +2 -2
- package/src/sitemap.js +3 -3
- package/src/static-cache.js +6 -3
- package/src/typecheck.js +105 -11
package/src/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The Hono app both servers start from.
|
|
2
2
|
//
|
|
3
3
|
// One function rather than two `new Hono()` calls, because the order things are
|
|
4
|
-
// registered in *is* the
|
|
4
|
+
// registered in *is* the behavior: a guard registered after the static handler
|
|
5
5
|
// does not guard a prerendered page. Two copies of that order is two servers
|
|
6
6
|
// that disagree, which has happened twice in this codebase already.
|
|
7
7
|
|
|
@@ -83,7 +83,7 @@ export function baseApp(options = {}) {
|
|
|
83
83
|
if (csrfOption) app.use('*', csrf(csrfOption === true ? undefined : csrfOption));
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* The one security header with no
|
|
86
|
+
* The one security header with no judgment in it.
|
|
87
87
|
*
|
|
88
88
|
* Without it a browser may sniff a response's bytes and decide the declared
|
|
89
89
|
* type was wrong, so a text file an app serves can be read as HTML and a
|
package/src/sitemap.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// else (a route with no `paths`, an endpoint, an error page) is not a page a
|
|
7
7
|
// crawler can reach by guessing, so it is left out.
|
|
8
8
|
|
|
9
|
+
import { urlFor } from './document.js';
|
|
10
|
+
|
|
9
11
|
/** The protocol's cap for one file. Past it the response is an index of files. */
|
|
10
12
|
const LIMIT = 50000;
|
|
11
13
|
|
|
@@ -56,9 +58,7 @@ export async function sitemapEntries(manifest, pages, { entries = [], exclude =
|
|
|
56
58
|
if (typeof page?.paths !== 'function') continue;
|
|
57
59
|
|
|
58
60
|
for (const params of (await page.paths()) ?? []) {
|
|
59
|
-
found.push({
|
|
60
|
-
path: route.pattern.replace(/:(\w+)(\{[^}]*\})?/g, (_, name) => String(params[name] ?? '')),
|
|
61
|
-
});
|
|
61
|
+
found.push({ path: urlFor(route, params) });
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
package/src/static-cache.js
CHANGED
|
@@ -41,7 +41,7 @@ const TYPES = {
|
|
|
41
41
|
* @property {number} onDisk left on disk because the budget ran out
|
|
42
42
|
* @property {number} encoded how many have a precompressed variant
|
|
43
43
|
* @property {Map<string, Entry|{ file: string }>} entries for the build, which
|
|
44
|
-
*
|
|
44
|
+
* serializes these for a runtime with no filesystem
|
|
45
45
|
* @property {(pathname: string) => Entry|null} get
|
|
46
46
|
*/
|
|
47
47
|
|
|
@@ -94,7 +94,7 @@ function load(dir, urlFor, { maxBytes = DEFAULT_MAX_BYTES } = {}) {
|
|
|
94
94
|
count: entries.size,
|
|
95
95
|
bytes,
|
|
96
96
|
onDisk,
|
|
97
|
-
/** For the build, which
|
|
97
|
+
/** For the build, which serializes these for runtimes with no filesystem. */
|
|
98
98
|
entries,
|
|
99
99
|
encoded: [...entries.values()].filter((e) => e.encodings?.size).length,
|
|
100
100
|
|
|
@@ -139,7 +139,10 @@ function read(file) {
|
|
|
139
139
|
* @returns {string} a quoted ETag
|
|
140
140
|
*/
|
|
141
141
|
export function etagOf(body) {
|
|
142
|
-
|
|
142
|
+
const digest = createHash('sha1').update(body).digest('base64url');
|
|
143
|
+
// Twenty characters of base64url is 120 bits, which is far more than a cache
|
|
144
|
+
// key needs and short enough to read in a header.
|
|
145
|
+
return `"${digest.slice(0, 20)}"`;
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
function variantSize(file) {
|
package/src/typecheck.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// and rewriting is where source mapping breaks down.
|
|
7
7
|
//
|
|
8
8
|
// JavaScript rather than TypeScript because a JSDoc `@type` in the author's own
|
|
9
|
-
// `<script props>` is
|
|
9
|
+
// `<script props>` is honored in a .js file and silently ignored in a .ts one.
|
|
10
10
|
//
|
|
11
11
|
// Shims are self-contained: route contexts and component props are inlined as
|
|
12
12
|
// type literals rather than imported. transclude-env.d.ts is written *from* the shims,
|
|
@@ -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
|
}
|