mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
feat(next): typed links
This commit is contained in:
parent
325a57c543
commit
f430225af6
10 changed files with 165 additions and 2 deletions
|
@ -7,4 +7,7 @@ import sitemap from '@astrojs/sitemap';
|
|||
export default defineConfig({
|
||||
site: 'https://example.com',
|
||||
integrations: [mdx(), sitemap()],
|
||||
experimental: {
|
||||
typedLinks: true
|
||||
}
|
||||
});
|
||||
|
|
|
@ -93,6 +93,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
|
|||
contentCollectionCache: false,
|
||||
clientPrerender: false,
|
||||
contentIntellisense: false,
|
||||
typedLinks: false,
|
||||
},
|
||||
} satisfies AstroUserConfig & { server: { open: boolean } };
|
||||
|
||||
|
@ -522,6 +523,7 @@ export const AstroConfigSchema = z.object({
|
|||
.boolean()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.contentIntellisense),
|
||||
typedLinks: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.typedLinks),
|
||||
})
|
||||
.strict(
|
||||
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`,
|
||||
|
@ -660,10 +662,14 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) {
|
|||
// Handle `base` and `image.endpoint.route` trailing slash based on `trailingSlash` config
|
||||
if (config.trailingSlash === 'never') {
|
||||
config.base = prependForwardSlash(removeTrailingForwardSlash(config.base));
|
||||
config.image.endpoint.route = prependForwardSlash(removeTrailingForwardSlash(config.image.endpoint.route));
|
||||
config.image.endpoint.route = prependForwardSlash(
|
||||
removeTrailingForwardSlash(config.image.endpoint.route),
|
||||
);
|
||||
} else if (config.trailingSlash === 'always') {
|
||||
config.base = prependForwardSlash(appendForwardSlash(config.base));
|
||||
config.image.endpoint.route = prependForwardSlash(appendForwardSlash(config.image.endpoint.route));
|
||||
config.image.endpoint.route = prependForwardSlash(
|
||||
appendForwardSlash(config.image.endpoint.route),
|
||||
);
|
||||
} else {
|
||||
config.base = prependForwardSlash(config.base);
|
||||
config.image.endpoint.route = prependForwardSlash(config.image.endpoint.route);
|
||||
|
|
|
@ -40,6 +40,7 @@ import { vitePluginMiddleware } from './middleware/vite-plugin.js';
|
|||
import { joinPaths } from './path.js';
|
||||
import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js';
|
||||
import { isObject } from './util.js';
|
||||
import { astroTypedLinks } from '../typed-links/vite-plugin-typed-links.js';
|
||||
|
||||
type CreateViteOptions = {
|
||||
settings: AstroSettings;
|
||||
|
@ -166,6 +167,7 @@ export async function createVite(
|
|||
vitePluginUserActions({ settings }),
|
||||
vitePluginServerIslands({ settings }),
|
||||
astroContainer(),
|
||||
astroTypedLinks({ settings }),
|
||||
],
|
||||
publicDir: fileURLToPath(settings.config.publicDir),
|
||||
root: fileURLToPath(settings.config.root),
|
||||
|
|
|
@ -33,6 +33,7 @@ import type { Logger } from '../logger/core.js';
|
|||
import { formatErrorMessage } from '../messages.js';
|
||||
import { createRouteManifest } from '../routing/index.js';
|
||||
import { ensureProcessNodeEnv } from '../util.js';
|
||||
import { syncTypedLinks } from '../../typed-links/sync.js';
|
||||
|
||||
export type SyncOptions = {
|
||||
/**
|
||||
|
@ -151,6 +152,7 @@ export async function syncInternal({
|
|||
});
|
||||
}
|
||||
syncAstroEnv(settings);
|
||||
syncTypedLinks(settings, manifest);
|
||||
|
||||
writeInjectedTypes(settings, fs);
|
||||
logger.info('types', `Generated ${dim(getTimeStat(timerStart, performance.now()))}`);
|
||||
|
|
7
packages/astro/src/typed-links/constants.ts
Normal file
7
packages/astro/src/typed-links/constants.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const VIRTUAL_MODULE_ID = 'astro:link';
|
||||
export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
||||
|
||||
const PKG_BASE = new URL('../../', import.meta.url);
|
||||
export const MODULE_TEMPLATE_URL = new URL('templates/typed-links/module.mjs', PKG_BASE);
|
||||
export const TYPES_TEMPLATE_URL = new URL('templates/typed-links/types.d.ts', PKG_BASE);
|
||||
export const TYPES_FILE = 'typed-links.d.ts';
|
55
packages/astro/src/typed-links/sync.ts
Normal file
55
packages/astro/src/typed-links/sync.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import type { AstroSettings, ManifestData } from '../types/astro.js';
|
||||
import { TYPES_FILE, TYPES_TEMPLATE_URL } from './constants.js';
|
||||
import { removeTrailingForwardSlash, appendForwardSlash } from '@astrojs/internal-helpers/path';
|
||||
|
||||
export function syncTypedLinks(settings: AstroSettings, manifest: ManifestData): void {
|
||||
if (!settings.config.experimental.typedLinks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: Array<{ route: string; params: Array<string> }> = [];
|
||||
|
||||
const { base, trailingSlash } = settings.config;
|
||||
for (const { route: _route, params } of manifest.routes) {
|
||||
const route = `${removeTrailingForwardSlash(base)}${_route}`;
|
||||
if (trailingSlash === 'always') {
|
||||
data.push({ route: appendForwardSlash(route), params });
|
||||
} else if (trailingSlash === 'never') {
|
||||
data.push({ route, params });
|
||||
} else {
|
||||
const r = appendForwardSlash(route);
|
||||
data.push({ route, params });
|
||||
if (route !== r) {
|
||||
data.push({ route: r, params });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let types = '';
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const { route, params } = data[i];
|
||||
|
||||
if (i > 0) {
|
||||
types += ' ';
|
||||
}
|
||||
|
||||
types += `"${route}": ${
|
||||
params.length === 0
|
||||
? 'never'
|
||||
: `{${params
|
||||
.map((key) => `"${key}": ${key.startsWith('...') ? 'string | undefined' : 'string'}`)
|
||||
.join('; ')}}`
|
||||
};`;
|
||||
|
||||
if (i !== data.length - 1) {
|
||||
types += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
const content = readFileSync(TYPES_TEMPLATE_URL, 'utf-8').replace('// @@LINKS@@', types);
|
||||
settings.injectedTypes.push({
|
||||
filename: TYPES_FILE,
|
||||
content,
|
||||
});
|
||||
}
|
31
packages/astro/src/typed-links/vite-plugin-typed-links.ts
Normal file
31
packages/astro/src/typed-links/vite-plugin-typed-links.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import type { Plugin } from 'vite';
|
||||
import type { AstroSettings } from '../types/astro.js';
|
||||
import { MODULE_TEMPLATE_URL, RESOLVED_VIRTUAL_MODULE_ID, VIRTUAL_MODULE_ID } from './constants.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
interface TypedLinksPluginParams {
|
||||
settings: AstroSettings;
|
||||
}
|
||||
|
||||
export function astroTypedLinks({ settings }: TypedLinksPluginParams): Plugin | undefined {
|
||||
if (!settings.config.experimental.typedLinks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const module = readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
|
||||
|
||||
return {
|
||||
name: 'astro-typed-links-plugin',
|
||||
enforce: 'pre',
|
||||
resolveId(id) {
|
||||
if (id === VIRTUAL_MODULE_ID) {
|
||||
return RESOLVED_VIRTUAL_MODULE_ID;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
||||
return module;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1630,6 +1630,9 @@ export interface AstroUserConfig {
|
|||
* To use this feature with the Astro VS Code extension, you must also enable the `astro.content-intellisense` option in your VS Code settings. For editors using the Astro language server directly, pass the `contentIntellisense: true` initialization parameter to enable this feature.
|
||||
*/
|
||||
contentIntellisense?: boolean;
|
||||
|
||||
/** TODO: */
|
||||
typedLinks?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
25
packages/astro/templates/typed-links/module.mjs
Normal file
25
packages/astro/templates/typed-links/module.mjs
Normal file
|
@ -0,0 +1,25 @@
|
|||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {{ params?: Record<string, string | undefined>; searchParams?: Record<string, string> | URLSearchParams; hash?: string; }} opts
|
||||
*/
|
||||
export const link = (path, opts = {}) => {
|
||||
let newPath = path;
|
||||
if (opts.params) {
|
||||
for (const [key, value] of Object.entries(opts.params)) {
|
||||
newPath = newPath.replace(`[${key}]`, value ?? '');
|
||||
}
|
||||
}
|
||||
if (opts.searchParams) {
|
||||
const searchParams =
|
||||
opts.searchParams instanceof URLSearchParams
|
||||
? opts.searchParams
|
||||
: new URLSearchParams(opts.searchParams);
|
||||
newPath += `?${searchParams.toString()}`;
|
||||
}
|
||||
if (opts.hash) {
|
||||
newPath += `#${opts.hash}`;
|
||||
}
|
||||
return newPath;
|
||||
};
|
29
packages/astro/templates/typed-links/types.d.ts
vendored
Normal file
29
packages/astro/templates/typed-links/types.d.ts
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
declare module 'astro:link' {
|
||||
interface Links {
|
||||
// @@LINKS@@
|
||||
}
|
||||
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
|
||||
type Opts<T> = Prettify<
|
||||
([T] extends [never]
|
||||
? {
|
||||
params?: never;
|
||||
}
|
||||
: {
|
||||
params: T;
|
||||
}) & {
|
||||
searchParams?: Record<string, string> | URLSearchParams;
|
||||
hash?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export function link<TPath extends keyof Links>(
|
||||
path: TPath,
|
||||
...[opts]: Links[TPath] extends never ? [opts?: Opts<Links[TPath]>] : [opts: Opts<Links[TPath]>]
|
||||
): string;
|
||||
}
|
||||
|
||||
// TODO: routePattern
|
Loading…
Reference in a new issue