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({
|
export default defineConfig({
|
||||||
site: 'https://example.com',
|
site: 'https://example.com',
|
||||||
integrations: [mdx(), sitemap()],
|
integrations: [mdx(), sitemap()],
|
||||||
|
experimental: {
|
||||||
|
typedLinks: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -93,6 +93,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
|
||||||
contentCollectionCache: false,
|
contentCollectionCache: false,
|
||||||
clientPrerender: false,
|
clientPrerender: false,
|
||||||
contentIntellisense: false,
|
contentIntellisense: false,
|
||||||
|
typedLinks: false,
|
||||||
},
|
},
|
||||||
} satisfies AstroUserConfig & { server: { open: boolean } };
|
} satisfies AstroUserConfig & { server: { open: boolean } };
|
||||||
|
|
||||||
|
@ -522,6 +523,7 @@ export const AstroConfigSchema = z.object({
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.contentIntellisense),
|
.default(ASTRO_CONFIG_DEFAULTS.experimental.contentIntellisense),
|
||||||
|
typedLinks: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.typedLinks),
|
||||||
})
|
})
|
||||||
.strict(
|
.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.`,
|
`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
|
// Handle `base` and `image.endpoint.route` trailing slash based on `trailingSlash` config
|
||||||
if (config.trailingSlash === 'never') {
|
if (config.trailingSlash === 'never') {
|
||||||
config.base = prependForwardSlash(removeTrailingForwardSlash(config.base));
|
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') {
|
} else if (config.trailingSlash === 'always') {
|
||||||
config.base = prependForwardSlash(appendForwardSlash(config.base));
|
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 {
|
} else {
|
||||||
config.base = prependForwardSlash(config.base);
|
config.base = prependForwardSlash(config.base);
|
||||||
config.image.endpoint.route = prependForwardSlash(config.image.endpoint.route);
|
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 { joinPaths } from './path.js';
|
||||||
import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js';
|
import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js';
|
||||||
import { isObject } from './util.js';
|
import { isObject } from './util.js';
|
||||||
|
import { astroTypedLinks } from '../typed-links/vite-plugin-typed-links.js';
|
||||||
|
|
||||||
type CreateViteOptions = {
|
type CreateViteOptions = {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
|
@ -166,6 +167,7 @@ export async function createVite(
|
||||||
vitePluginUserActions({ settings }),
|
vitePluginUserActions({ settings }),
|
||||||
vitePluginServerIslands({ settings }),
|
vitePluginServerIslands({ settings }),
|
||||||
astroContainer(),
|
astroContainer(),
|
||||||
|
astroTypedLinks({ settings }),
|
||||||
],
|
],
|
||||||
publicDir: fileURLToPath(settings.config.publicDir),
|
publicDir: fileURLToPath(settings.config.publicDir),
|
||||||
root: fileURLToPath(settings.config.root),
|
root: fileURLToPath(settings.config.root),
|
||||||
|
|
|
@ -33,6 +33,7 @@ import type { Logger } from '../logger/core.js';
|
||||||
import { formatErrorMessage } from '../messages.js';
|
import { formatErrorMessage } from '../messages.js';
|
||||||
import { createRouteManifest } from '../routing/index.js';
|
import { createRouteManifest } from '../routing/index.js';
|
||||||
import { ensureProcessNodeEnv } from '../util.js';
|
import { ensureProcessNodeEnv } from '../util.js';
|
||||||
|
import { syncTypedLinks } from '../../typed-links/sync.js';
|
||||||
|
|
||||||
export type SyncOptions = {
|
export type SyncOptions = {
|
||||||
/**
|
/**
|
||||||
|
@ -151,6 +152,7 @@ export async function syncInternal({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
syncAstroEnv(settings);
|
syncAstroEnv(settings);
|
||||||
|
syncTypedLinks(settings, manifest);
|
||||||
|
|
||||||
writeInjectedTypes(settings, fs);
|
writeInjectedTypes(settings, fs);
|
||||||
logger.info('types', `Generated ${dim(getTimeStat(timerStart, performance.now()))}`);
|
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.
|
* 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;
|
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