From 364150ada1d2c44026c15515c95a6d98923f6c8f Mon Sep 17 00:00:00 2001 From: leegeunhyeok Date: Tue, 30 Jun 2026 12:17:16 +0900 Subject: [PATCH] feat: glob import --- examples/0.84/rollipop-env.d.ts | 1 + examples/0.84/src/navigation/AppNavigator.tsx | 35 ++++- examples/0.84/src/navigation/types.ts | 6 +- examples/0.84/src/pages/details.ts | 1 + examples/0.84/src/pages/index.ts | 1 + examples/0.84/src/screens/HomeScreen.tsx | 2 +- packages/rollipop/client.d.ts | 6 +- packages/rollipop/import-glob.d.ts | 129 ++++++++++++++++++ packages/rollipop/package.json | 1 + .../src/core/plugins/import-glob-plugin.ts | 14 ++ packages/rollipop/src/core/plugins/index.ts | 1 + packages/rollipop/src/core/rolldown.ts | 12 ++ 12 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 examples/0.84/src/pages/details.ts create mode 100644 examples/0.84/src/pages/index.ts create mode 100644 packages/rollipop/import-glob.d.ts create mode 100644 packages/rollipop/src/core/plugins/import-glob-plugin.ts diff --git a/examples/0.84/rollipop-env.d.ts b/examples/0.84/rollipop-env.d.ts index 5b6ae478..cb4cdc98 100644 --- a/examples/0.84/rollipop-env.d.ts +++ b/examples/0.84/rollipop-env.d.ts @@ -6,4 +6,5 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; + readonly glob: ImportGlobFunction; } diff --git a/examples/0.84/src/navigation/AppNavigator.tsx b/examples/0.84/src/navigation/AppNavigator.tsx index 927a3f7f..7c68ea47 100644 --- a/examples/0.84/src/navigation/AppNavigator.tsx +++ b/examples/0.84/src/navigation/AppNavigator.tsx @@ -3,12 +3,27 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useReactNavigationDevTools } from '@rozenite/react-navigation-plugin'; import { useRef } from 'react'; -import { DetailsScreen } from '../screens/DetailsScreen'; -import { HomeScreen } from '../screens/HomeScreen'; import type { RootStackParamList } from './types'; +interface Screen { + path: string; + component: React.ComponentType; +} + const RootStack = createNativeStackNavigator(); +const pages = import.meta.glob('../pages/*', { import: 'default', eager: true }); +const screens = Object.entries(pages).map(([path, module]) => { + return { + path: parseScreenPath(path), + component: module, + } as Screen; +}); + +function parseScreenPath(path: string) { + return path.split('/').pop()?.split('.').shift(); +} + export function AppNavigator() { const navigationRef = useRef>(null!); @@ -17,13 +32,19 @@ export function AppNavigator() { return ( ({ + headerShown: props.route.name !== 'index', headerTitle: '', - }} + })} > - - + {screens.map((screen) => ( + + ))} ); diff --git a/examples/0.84/src/navigation/types.ts b/examples/0.84/src/navigation/types.ts index 735ad453..7726e794 100644 --- a/examples/0.84/src/navigation/types.ts +++ b/examples/0.84/src/navigation/types.ts @@ -1,5 +1,7 @@ +/** + * @TODO Sync with the pages/*.ts files. + */ export type RootStackParamList = { - home: undefined; + index: undefined; details: undefined; - test_suites: undefined; }; diff --git a/examples/0.84/src/pages/details.ts b/examples/0.84/src/pages/details.ts new file mode 100644 index 00000000..fa56de05 --- /dev/null +++ b/examples/0.84/src/pages/details.ts @@ -0,0 +1 @@ +export { DetailsScreen as default } from '../screens/DetailsScreen'; diff --git a/examples/0.84/src/pages/index.ts b/examples/0.84/src/pages/index.ts new file mode 100644 index 00000000..810b65dd --- /dev/null +++ b/examples/0.84/src/pages/index.ts @@ -0,0 +1 @@ +export { HomeScreen as default } from '../screens/HomeScreen'; diff --git a/examples/0.84/src/screens/HomeScreen.tsx b/examples/0.84/src/screens/HomeScreen.tsx index 9ea5ad4b..884f4fe9 100644 --- a/examples/0.84/src/screens/HomeScreen.tsx +++ b/examples/0.84/src/screens/HomeScreen.tsx @@ -16,7 +16,7 @@ const breathe: CSSAnimationKeyframes = { }, }; -type HomeNavigation = NativeStackNavigationProp; +type HomeNavigation = NativeStackNavigationProp; export function HomeScreen() { const navigation = useNavigation(); diff --git a/packages/rollipop/client.d.ts b/packages/rollipop/client.d.ts index 8619b6cc..7490aee6 100644 --- a/packages/rollipop/client.d.ts +++ b/packages/rollipop/client.d.ts @@ -4,6 +4,10 @@ interface ImportMetaEnv { } interface ImportMeta { - hot?: import('./dist').HMRContext; + glob: import('./import-glob').ImportGlobFunction; env: ImportMetaEnv; + /** + * Only available in development mode. + */ + hot?: import('./dist').HMRContext; } diff --git a/packages/rollipop/import-glob.d.ts b/packages/rollipop/import-glob.d.ts new file mode 100644 index 00000000..c9432e90 --- /dev/null +++ b/packages/rollipop/import-glob.d.ts @@ -0,0 +1,129 @@ +/** + * @see https://github.com/vitejs/vite/blob/3dcf7bd9418e02282cbfa0254ace59fe702c11bd/packages/vite/types/importGlob.d.ts + */ + +// make input suggestions work +type StringQueryType = `?${keyof KnownAsTypeMap}` | (string & {}); +type BaseQueryType = StringQueryType | Record; + +export interface ImportGlobOptions< + Eager extends boolean, + AsType extends string, + QueryType extends BaseQueryType, +> { + /** + * Import type for the import url. + * + * @deprecated Use `query` instead, e.g. `as: 'url'` -> `query: '?url', import: 'default'` + */ + as?: AsType; + /** + * Import as static or dynamic + * + * @default false + */ + eager?: Eager; + /** + * Import only the specific named export. Set to `default` to import the default export. + */ + import?: string; + /** + * Custom queries + */ + query?: QueryType; + /** + * Search files also inside `node_modules/` and hidden directories (e.g. `.git/`). This might have impact on performance. + * + * @default false + */ + exhaustive?: boolean; + /** + * Base path to resolve relative paths. + */ + base?: string; + /** + * Whether the glob pattern matching should be case-sensitive. Defaults to `true`. + */ + caseSensitive?: boolean; +} + +export type ImportGlobOptionsWithoutAs< + Eager extends boolean, + QueryType extends BaseQueryType, +> = Omit, 'as'> & { + as?: never; +}; + +export type GeneralImportGlobOptions = ImportGlobOptions; + +/** + * Declare Worker in case DOM is not added to the tsconfig lib causing + * Worker interface is not defined. For developers with DOM lib added, + * the Worker interface will be merged correctly. + */ +declare global { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface Worker {} +} + +export interface KnownAsTypeMap { + raw: string; + url: string; + worker: Worker; +} + +type KnownQueryTypeMap = { + [K in keyof KnownAsTypeMap as `?${K}`]: KnownAsTypeMap[K]; +}; + +export interface ImportGlobFunction { + /** + * Import a list of files with a glob pattern. + * + * Overload 1A: No generic provided, infer the type from `eager` and `query` + */ + < + Eager extends boolean, + Query extends BaseQueryType, + T = Query extends keyof KnownQueryTypeMap ? KnownQueryTypeMap[Query] : unknown, + >( + glob: string | string[], + options?: ImportGlobOptionsWithoutAs, + ): (Eager extends true ? true : false) extends true + ? Record + : Record Promise>; + /** + * Import a list of files with a glob pattern. + * + * Overload 1B: No generic provided, infer the type from `eager` and `as` + * (deprecated, use `query` instead) + */ + < + Eager extends boolean, + As extends string, + T = As extends keyof KnownAsTypeMap ? KnownAsTypeMap[As] : unknown, + >( + glob: string | string[], + options?: ImportGlobOptions, + ): (Eager extends true ? true : false) extends true + ? Record + : Record Promise>; + /** + * Import a list of files with a glob pattern. + * + * Overload 2: Module generic provided, infer the type from `eager: false` + */ + ( + glob: string | string[], + options?: ImportGlobOptions, + ): Record Promise>; + /** + * Import a list of files with a glob pattern. + * + * Overload 3: Module generic provided, infer the type from `eager: true` + */ + ( + glob: string | string[], + options: ImportGlobOptions, + ): Record; +} diff --git a/packages/rollipop/package.json b/packages/rollipop/package.json index fba2544b..a8b4f812 100644 --- a/packages/rollipop/package.json +++ b/packages/rollipop/package.json @@ -16,6 +16,7 @@ "bin": "./bin/index.js", "files": [ "client.d.ts", + "import-glob.d.ts", "bin", "dist", "skills", diff --git a/packages/rollipop/src/core/plugins/import-glob-plugin.ts b/packages/rollipop/src/core/plugins/import-glob-plugin.ts new file mode 100644 index 00000000..3dfd9b63 --- /dev/null +++ b/packages/rollipop/src/core/plugins/import-glob-plugin.ts @@ -0,0 +1,14 @@ +import type * as rolldown from '@rollipop/rolldown'; +import { viteImportGlobPlugin as importGlob } from '@rollipop/rolldown/experimental'; + +export interface ImportGlobPluginOptions { + root?: string; + sourcemap?: boolean; + restoreQueryExtension?: boolean; +} + +function importGlobPlugin(options: ImportGlobPluginOptions): rolldown.Plugin { + return importGlob(options); +} + +export { importGlobPlugin as importGlob }; diff --git a/packages/rollipop/src/core/plugins/index.ts b/packages/rollipop/src/core/plugins/index.ts index e5ad52e4..b3c84bfd 100644 --- a/packages/rollipop/src/core/plugins/index.ts +++ b/packages/rollipop/src/core/plugins/index.ts @@ -5,3 +5,4 @@ export * from './swc-plugin'; export * from './reporter-plugin'; export * from './dev-server-plugin'; export * from './analyze-plugin'; +export * from './import-glob-plugin'; diff --git a/packages/rollipop/src/core/rolldown.ts b/packages/rollipop/src/core/rolldown.ts index 87a11507..2a7ed10e 100644 --- a/packages/rollipop/src/core/rolldown.ts +++ b/packages/rollipop/src/core/rolldown.ts @@ -31,6 +31,7 @@ import { type BabelPluginOptions, type DevServerPluginOptions, type EntryPluginOptions, + type ImportGlobPluginOptions, type ReactNativePluginOptions, type ReporterPluginOptions, type SwcPluginOptions, @@ -38,6 +39,7 @@ import { babel, devServer, entry, + importGlob, reactNative, reporter, swc, @@ -170,6 +172,7 @@ export async function resolveRolldownOptions( ); const entryPluginOptions = resolveEntryPluginOptions(config); + const importGlobPluginOptions = resolveImportGlobPluginOptions(config); const reactNativePluginOptions = await resolveReactNativePluginOptions( config, context, @@ -200,6 +203,7 @@ export async function resolveRolldownOptions( }, plugins: withTransformBoundary(context, [ entry(entryPluginOptions), + importGlob(importGlobPluginOptions), reactNative(reactNativePluginOptions), babel(babelPluginOptions), swc(swcPluginOptions), @@ -279,6 +283,14 @@ function resolveEntryPluginOptions(config: ResolvedConfig): EntryPluginOptions { }; } +function resolveImportGlobPluginOptions(config: ResolvedConfig): ImportGlobPluginOptions { + return { + root: config.root, + sourcemap: config.mode === 'development', + restoreQueryExtension: false, + }; +} + async function resolveReactNativePluginOptions( config: ResolvedConfig, context: BundlerContext,