UNPKG

imba

Version:

Intuitive and powerful language for building webapps that fly

176 lines (157 loc) 4.92 kB
/* eslint-disable no-unused-vars */ import { createFilter } from '@rollup/pluginutils'; import { Arrayable, ResolvedOptions } from './options'; import { normalizePath } from 'vite'; import * as fs from 'node:fs'; import { URL, URLSearchParams, pathToFileURL } from 'node:url' const VITE_FS_PREFIX = '/@fs/'; const IS_WINDOWS = process.platform === 'win32'; export type ImbaQueryTypes = 'style' | 'script'; export const queryRE = /\?.*$/s export const hashRE = /#.*$/s export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '') export interface RequestQuery { // our own imba?: boolean; iife?: boolean; worker?: boolean; web?: boolean; type?: ImbaQueryTypes; // vite specific url?: boolean; raw?: boolean; } export interface ImbaRequest { id: string; cssId: string; filename: string; normalizedFilename: string; query: RequestQuery; timestamp: number; ssr: boolean; } function splitId(id: string) { const parts = id.split(`?`, 2); const filename = parts[0]; const rawQuery = parts[1]; return { filename, rawQuery }; } function parseToImbaRequest( id: string, filename: string, rawQuery: string, root: string, timestamp: number, ssr: boolean ): ImbaRequest | undefined { const query = parseRequestQuery(rawQuery); if (query.url || query.raw) { // skip requests with special vite tags return; } const normalizedFilename = normalize(filename, root); const cssId = createVirtualImportId(filename, root, 'style'); return { id, filename, normalizedFilename, cssId, query, timestamp, ssr }; } export const requestQuerySplitRE = /\?(?!.*[\/|\}])/; export function parseRequest(id: string): Record<string, string> | null { const [_, search] = id.split(requestQuerySplitRE, 2); if (!search) { return null; } return Object.fromEntries(new URLSearchParams(search)); } function createVirtualImportId(filename: string, root: string, type: ImbaQueryTypes) { const parts = ['imba', `type=${type}`]; if (type === 'style') { parts.push('lang.css'); } if (existsInRoot(filename, root)) { filename = root + filename; } else if (filename.startsWith(VITE_FS_PREFIX)) { filename = IS_WINDOWS ? filename.slice(VITE_FS_PREFIX.length) // remove /@fs/ from /@fs/C:/... : filename.slice(VITE_FS_PREFIX.length - 1); // remove /@fs from /@fs/home/user } // return same virtual id format as vite-plugin-vue eg ...App.imba?imba&type=style&lang.css return `${filename}?${parts.join('&')}`; } export function parseRequestQuery(rawQuery: string): RequestQuery { const query = Object.fromEntries(new URLSearchParams(rawQuery)); for (const key in query) { if (query[key] === '') { // @ts-ignore query[key] = true; } } return query as RequestQuery; } /** * posixify and remove root at start * * @param filename * @param normalizedRoot */ export function normalize(filename: string, normalizedRoot: string) { return stripRoot(normalizePath(filename), normalizedRoot); } function existsInRoot(filename: string, root: string) { if (filename.startsWith(VITE_FS_PREFIX)) { return false; // vite already tagged it as out of root } return fs.existsSync(root + filename); } function stripRoot(normalizedFilename: string, normalizedRoot: string) { return normalizedFilename.startsWith(normalizedRoot + '/') ? normalizedFilename.slice(normalizedRoot.length) : normalizedFilename; } function buildFilter( include: Arrayable<string> | undefined, exclude: Arrayable<string> | undefined, extensions: string[] ): (filename: string) => boolean { const rollupFilter = createFilter(include, exclude); return (filename) => rollupFilter(filename) && extensions.some((ext) => { const [name] = filename.split(`?`, 2) return name.endsWith(ext) }); } export type IdParser = (id: string, ssr: boolean, timestamp?: number) => ImbaRequest | undefined; export function buildIdParser(options: ResolvedOptions): IdParser { const { include, exclude, extensions, root } = options; const normalizedRoot = normalizePath(root); const filter = buildFilter(include, exclude, extensions!); return (id, ssr, timestamp = Date.now()) => { const { filename, rawQuery } = splitId(id); if (filter(filename)) { return parseToImbaRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr); } }; } export function slash(p: string): string { return p.replace(/\\/g, '/') } export function injectQuery(url: string, queryToInject: string): string { // encode percents for consistent behavior with pathToFileURL // see #2614 for details let resolvedUrl = new URL(url.replace(/%/g, '%25'), 'relative:///') if (resolvedUrl.protocol !== 'relative:') { resolvedUrl = pathToFileURL(url) } const { search, hash } = resolvedUrl let pathname = cleanUrl(url) pathname = IS_WINDOWS ? slash(pathname) : pathname return `${pathname}?${queryToInject}${search ? `&` + search.slice(1) : ''}${ hash ?? '' }` }