0
Fork 0
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:
Florian Lefebvre 2024-09-20 15:08:07 +02:00
parent 325a57c543
commit f430225af6
10 changed files with 165 additions and 2 deletions

View file

@ -7,4 +7,7 @@ import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [mdx(), sitemap()],
experimental: {
typedLinks: true
}
});

View file

@ -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);

View file

@ -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),

View file

@ -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()))}`);

View 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';

View 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,
});
}

View 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;
}
},
};
}

View file

@ -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;
};
}

View 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;
};

View 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