@rsc-kit/core 0.8.0 → 0.9.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.
@@ -0,0 +1,69 @@
1
+ export interface IconDescriptor {
2
+ url: string | URL;
3
+ type?: string;
4
+ sizes?: string;
5
+ color?: string;
6
+ rel?: string;
7
+ media?: string;
8
+ fetchPriority?: 'high' | 'low' | 'auto';
9
+ }
10
+ export type IconURL = string | URL;
11
+ export interface Icons {
12
+ icon?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];
13
+ apple?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];
14
+ shortcut?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];
15
+ other?: IconDescriptor | IconDescriptor[];
16
+ }
17
+ /** A layout's title, wrapping the titles of the pages beneath it. */
18
+ export interface TitleTemplate {
19
+ /** `%s` stands in for the page's own title. */
20
+ template?: string;
21
+ /** Used by a page that exports no title of its own. */
22
+ default?: string;
23
+ }
24
+ export interface Metadata {
25
+ /** A string on a page; a template on a layout, applied to the pages below it. */
26
+ title?: string | TitleTemplate;
27
+ description?: string;
28
+ keywords?: string | string[];
29
+ author?: string;
30
+ robots?: string;
31
+ icons?: IconURL | (IconURL | IconDescriptor)[] | Icons | null;
32
+ 'og:title'?: string;
33
+ 'og:description'?: string;
34
+ 'og:image'?: string;
35
+ 'og:url'?: string;
36
+ 'og:type'?: string;
37
+ 'og:site_name'?: string;
38
+ 'twitter:card'?: string;
39
+ 'twitter:title'?: string;
40
+ 'twitter:description'?: string;
41
+ 'twitter:image'?: string;
42
+ 'twitter:site'?: string;
43
+ /**
44
+ * Any other meta tag, by name.
45
+ *
46
+ * other: { 'fb:app_id': '123', 'theme-color': '#000' }
47
+ *
48
+ * Here rather than alongside the named keys, and that is what makes the rest
49
+ * of this interface worth annotating. An index signature on the interface
50
+ * itself made every key legal — so `titel` was accepted in silence, and an
51
+ * editor offered no completions at all, because with any identifier valid
52
+ * TypeScript reads an unfinished key as a shorthand property and goes looking
53
+ * for a variable by that name.
54
+ */
55
+ other?: Record<string, string | string[] | null | undefined>;
56
+ }
57
+ /**
58
+ * Metadata that depends on the request.
59
+ *
60
+ * Receives the same awaitables a page does, so one shape is learned rather
61
+ * than two:
62
+ *
63
+ * export const generateMetadata: GenerateMetadata<{ slug: string }> =
64
+ * async ({ params }) => ({ title: (await params).slug })
65
+ */
66
+ export type GenerateMetadata<P = Record<string, string>> = (args: {
67
+ params: Promise<P>;
68
+ searchParams: Promise<URLSearchParams>;
69
+ }) => Metadata | Promise<Metadata>;
@@ -0,0 +1,17 @@
1
+ // What a page says about itself, as importable types.
2
+ //
3
+ // import type { Metadata } from '@rsc-kit/core/metadata'
4
+ //
5
+ // export const metadata: Metadata = { title: 'Orders' }
6
+ //
7
+ // Imported rather than ambient, and that is the whole point of the move. An
8
+ // ambient declaration has to be COPIED into the project, which means it is not
9
+ // there until the build has run once — so a freshly cloned app reports "Cannot
10
+ // find name 'Metadata'" on every page until someone runs the dev server. An
11
+ // import resolves from node_modules the moment dependencies are installed.
12
+ //
13
+ // The ambient names still work. `.rsc-kit/rsc-types.d.ts` now aliases these
14
+ // rather than restating them, so there is one definition and two ways to reach
15
+ // it.
16
+ export {};
17
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,6DAA6D;AAC7D,EAAE;AACF,4DAA4D;AAC5D,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,MAAM","sourcesContent":["// What a page says about itself, as importable types.\n//\n// import type { Metadata } from '@rsc-kit/core/metadata'\n//\n// export const metadata: Metadata = { title: 'Orders' }\n//\n// Imported rather than ambient, and that is the whole point of the move. An\n// ambient declaration has to be COPIED into the project, which means it is not\n// there until the build has run once — so a freshly cloned app reports \"Cannot\n// find name 'Metadata'\" on every page until someone runs the dev server. An\n// import resolves from node_modules the moment dependencies are installed.\n//\n// The ambient names still work. `.rsc-kit/rsc-types.d.ts` now aliases these\n// rather than restating them, so there is one definition and two ways to reach\n// it.\n\nexport interface IconDescriptor {\n url: string | URL\n type?: string\n sizes?: string\n color?: string\n rel?: string\n media?: string\n fetchPriority?: 'high' | 'low' | 'auto'\n}\n\nexport type IconURL = string | URL\n\nexport interface Icons {\n icon?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[]\n apple?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[]\n shortcut?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[]\n other?: IconDescriptor | IconDescriptor[]\n}\n\n/** A layout's title, wrapping the titles of the pages beneath it. */\nexport interface TitleTemplate {\n /** `%s` stands in for the page's own title. */\n template?: string\n /** Used by a page that exports no title of its own. */\n default?: string\n}\n\nexport interface Metadata {\n /** A string on a page; a template on a layout, applied to the pages below it. */\n title?: string | TitleTemplate\n description?: string\n keywords?: string | string[]\n author?: string\n robots?: string\n icons?: IconURL | (IconURL | IconDescriptor)[] | Icons | null\n 'og:title'?: string\n 'og:description'?: string\n 'og:image'?: string\n 'og:url'?: string\n 'og:type'?: string\n 'og:site_name'?: string\n 'twitter:card'?: string\n 'twitter:title'?: string\n 'twitter:description'?: string\n 'twitter:image'?: string\n 'twitter:site'?: string\n\n /**\n * Any other meta tag, by name.\n *\n * other: { 'fb:app_id': '123', 'theme-color': '#000' }\n *\n * Here rather than alongside the named keys, and that is what makes the rest\n * of this interface worth annotating. An index signature on the interface\n * itself made every key legal — so `titel` was accepted in silence, and an\n * editor offered no completions at all, because with any identifier valid\n * TypeScript reads an unfinished key as a shorthand property and goes looking\n * for a variable by that name.\n */\n other?: Record<string, string | string[] | null | undefined>\n}\n\n/**\n * Metadata that depends on the request.\n *\n * Receives the same awaitables a page does, so one shape is learned rather\n * than two:\n *\n * export const generateMetadata: GenerateMetadata<{ slug: string }> =\n * async ({ params }) => ({ title: (await params).slug })\n */\nexport type GenerateMetadata<P = Record<string, string>> = (args: {\n params: Promise<P>\n searchParams: Promise<URLSearchParams>\n}) => Metadata | Promise<Metadata>\n"]}
package/dist/vite.js CHANGED
@@ -413,10 +413,19 @@ function warnIfTypesUnreachable() {
413
413
  const include = JSON.parse(text).include;
414
414
  if (!Array.isArray(include))
415
415
  return;
416
- if (include.some((entry) => typeof entry === 'string' && entry.includes('.rsc-kit')))
417
- return;
418
- log(`tsconfig.json does not include .rsc-kit, where the generated types are written.\n` +
419
- ` Add ".rsc-kit/**/*" to "include", or typed routes and rpc() fall back to string.`);
416
+ const covers = (what) => include.some((entry) => typeof entry === 'string' && entry.includes(what));
417
+ if (!covers('.rsc-kit')) {
418
+ log(`tsconfig.json does not include .rsc-kit, where the generated types are written.\n` +
419
+ ` Add ".rsc-kit/**/*" to "include", or typed routes and rpc() fall back to string.`);
420
+ }
421
+ // Only when there is something there to check. An app with no api routes
422
+ // has nothing to be warned about, and a warning it cannot act on is one it
423
+ // learns to scroll past.
424
+ if (existsSync(join(projectRoot, 'server')) && !covers('server')) {
425
+ log(`tsconfig.json does not include server/, where your api handlers live.\n` +
426
+ ` Add "server/**/*" to "include", or they are not type-checked at all — ` +
427
+ `a handler returning the wrong shape builds and deploys without complaint.`);
428
+ }
420
429
  }
421
430
  catch {
422
431
  // An unparseable tsconfig is the project's own problem, not this one's.
@@ -451,10 +460,6 @@ function writeHostBindings(manifest) {
451
460
  // and a typecheck cannot see it. Written whether or not there are actions:
452
461
  // server components call it directly too.
453
462
  writeFileSync(join(typesDir, 'rsc-env.d.ts'), renderHostGlobalTypes());
454
- // The engine's own ambient types, copied where the app's typechecker will
455
- // see them. Deliberately a separate file from the one above: this one is
456
- // the engine's and identical everywhere, that one is generated from how
457
- // this host is configured.
458
463
  // The urls this build found, so a link to a page that does not exist fails
459
464
  // the typecheck instead of the browser.
460
465
  writeFileSync(join(typesDir, 'rsc-routes.d.ts'), renderRouteTypes(manifest));
@@ -463,10 +468,6 @@ function writeHostBindings(manifest) {
463
468
  // an app-authored one goes stale — the first version named only RscEngine,
464
469
  // which typechecks a server and fails a prerender script.
465
470
  writeFileSync(join(typesDir, 'rsc-engine.d.ts'), ENGINE_TYPES);
466
- const engineTypes = join(packageDir, 'types.d.ts');
467
- if (existsSync(engineTypes)) {
468
- writeFileSync(join(typesDir, 'rsc-types.d.ts'), readFileSync(engineTypes, 'utf-8'));
469
- }
470
471
  warnIfTypesUnreachable();
471
472
  const target = join(sourceDir, 'server-actions.generated.ts');
472
473
  // A host with no functions of its own leaves no file behind: kept, its
@@ -1492,7 +1493,14 @@ async function renderTree(
1492
1493
  if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
1493
1494
  }
1494
1495
  if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
1495
- for (const [k, v] of Object.entries(md)) {
1496
+ // other is flattened in beside the named keys, because it is a place to put
1497
+ // meta tags rather than a meta tag by that name. A key at the top level
1498
+ // still renders — the type no longer invites one, but an app written
1499
+ // against the old shape must not silently lose its tags.
1500
+ const named = Object.entries(md).filter(([k]) => k !== 'other')
1501
+ const extra = Object.entries((md.other ?? {}) as Record<string, unknown>)
1502
+
1503
+ for (const [k, v] of [...named, ...extra]) {
1496
1504
  if (k === 'title' || k === 'description' || v == null) continue
1497
1505
  head.push(createElement('meta', { key: '__m_' + k, name: k, content: String(v) }))
1498
1506
  }
@@ -2017,12 +2025,31 @@ export async function resolveMetadata(
2017
2025
  : {}
2018
2026
 
2019
2027
  // Non-title metadata: layout defaults (outer→inner), page overrides.
2028
+ //
2029
+ // other merges per key rather than being replaced, so a page adding one
2030
+ // custom tag keeps the ones its layout set. Assigning it like any other key
2031
+ // would mean a root layout's theme-color disappearing from every page that
2032
+ // happened to declare one of its own.
2020
2033
  const merged: Record<string, unknown> = {}
2034
+ const other: Record<string, unknown> = {}
2035
+
2036
+ const take = (from: Record<string, unknown>) => {
2037
+ for (const [k, v] of Object.entries(from)) {
2038
+ if (k === 'title') continue
2039
+ if (k === 'other') Object.assign(other, v as Record<string, unknown>)
2040
+ else merged[k] = v
2041
+ }
2042
+ }
2043
+
2021
2044
  for (const l of layouts) {
2022
2045
  const s = metadataMap[l.component]?.static
2023
- if (s) for (const [k, v] of Object.entries(s)) if (k !== 'title') merged[k] = v
2046
+
2047
+ if (s) take(s as Record<string, unknown>)
2024
2048
  }
2025
- for (const [k, v] of Object.entries(page)) if (k !== 'title') merged[k] = v
2049
+
2050
+ take(page)
2051
+
2052
+ if (Object.keys(other).length > 0) merged.other = other
2026
2053
 
2027
2054
  // Title: the page title with the NEAREST layout title.template applied; if the
2028
2055
  // page has no title, the nearest layout default/string title.