This commit is contained in:
2022-07-21 03:28:35 +00:00
parent d7c883d6df
commit 51b34b0e1d
30103 changed files with 4152204 additions and 23 deletions
+5
View File
@@ -0,0 +1,5 @@
import loader from './loader';
export * from './types';
export default loader;
+22
View File
@@ -0,0 +1,22 @@
import {errorMessage, isFunction, loadResources} from './utils';
import type {Loader, LoaderCallback} from '.';
const loader: Loader = function (source) {
this.cacheable();
const callback = this.async();
if (!isFunction<LoaderCallback>(callback)) {
throw new Error(errorMessage.syncCompilation);
}
/* istanbul ignore if: not possible to test */
if (typeof source !== 'string') {
throw new Error(errorMessage.impossible);
}
void loadResources(this, source, callback);
};
export default loader;
+40
View File
@@ -0,0 +1,40 @@
import type validate from 'schema-utils';
type Schema = Parameters<typeof validate>[0];
export const schema: Schema = {
type: 'object',
properties: {
patterns: {
anyOf: [
{type: 'string'},
{
type: 'array',
uniqueItems: true,
items: {
type: 'string',
},
},
],
},
injector: {
anyOf: [
{
type: 'string',
enum: ['prepend', 'append'],
},
{
instanceof: 'Function',
},
],
},
globOptions: {
type: 'object',
},
resolveUrl: {
type: 'boolean',
},
},
required: ['patterns'],
additionalProperties: false,
};
+39
View File
@@ -0,0 +1,39 @@
import type {loader} from 'webpack';
import type glob from 'glob';
export type Loader = loader.Loader;
export type LoaderContext = loader.LoaderContext;
export type LoaderCallback = loader.loaderCallback;
export type StyleResourcesFileFormat = 'css' | 'sass' | 'scss' | 'less' | 'styl';
export interface StyleResource {
file: string;
content: string;
}
export type StyleResources = StyleResource[];
export type StyleResourcesFunctionalInjector = (
this: LoaderContext,
source: string,
resources: StyleResources,
) => string | Promise<string>;
export type StyleResourcesInjector = 'prepend' | 'append' | StyleResourcesFunctionalInjector;
export type StyleResourcesNormalizedInjector = StyleResourcesFunctionalInjector;
export interface StyleResourcesLoaderOptions {
patterns: string | string[];
injector?: StyleResourcesInjector;
globOptions?: glob.IOptions;
resolveUrl?: boolean;
}
export interface StyleResourcesLoaderNormalizedOptions extends NonNullable<StyleResourcesLoaderOptions> {
patterns: string[];
injector: StyleResourcesNormalizedInjector;
}
+15
View File
@@ -0,0 +1,15 @@
import type {StyleResourcesFileFormat} from '../types';
export const PACKAGE_NAME = 'style-resources-loader';
export const ISSUES_URL = `https://github.com/yenshih/${PACKAGE_NAME}/issues`;
export const LOADER_NAME = PACKAGE_NAME.split('-')
.map(word => `${word[0].toUpperCase()}${word.slice(1)}`)
.join(' ');
export const VALIDATION_BASE_DATA_PATH = 'options';
export const SUPPORTED_FILE_FORMATS: StyleResourcesFileFormat[] = ['css', 'sass', 'scss', 'less', 'styl'];
export const SUPPORTED_FILE_EXTS = SUPPORTED_FILE_FORMATS.map(type => `.${type}`);
+17
View File
@@ -0,0 +1,17 @@
import {PACKAGE_NAME, ISSUES_URL} from './constants';
const formatErrorMessage = (message: string) => `[${PACKAGE_NAME}] ${message}`;
const messageByType = {
impossible: `This error is caused by a bug. Please file an issue: ${ISSUES_URL}.`,
syncCompilation: 'Synchronous compilation is not supported.',
invalidInjectorReturn: 'Expected options.injector(...) returns a string. Instead received number.',
};
export const errorMessage = Object.entries(messageByType).reduce(
(errorMessage, [type, message]) => ({
...errorMessage,
[type]: formatErrorMessage(message),
}),
messageByType,
);
+26
View File
@@ -0,0 +1,26 @@
import fs from 'fs';
import util from 'util';
import type {LoaderContext, StyleResource, StyleResourcesLoaderNormalizedOptions} from '../types';
import {matchFiles} from './match-files';
import {resolveImportUrl} from './resolve-import-url';
export const getResources = async (ctx: LoaderContext, options: StyleResourcesLoaderNormalizedOptions) => {
const {resolveUrl} = options;
const files = await matchFiles(ctx, options);
files.forEach(file => ctx.dependency(file));
const resources = await Promise.all(
files.map(async file => {
const content = await util.promisify(fs.readFile)(file, 'utf8');
const resource: StyleResource = {file, content};
return resolveUrl ? resolveImportUrl(ctx, resource) : resource;
}),
);
return resources;
};
+10
View File
@@ -0,0 +1,10 @@
export * from './constants';
export * from './error-message';
export * from './get-resources';
export * from './inject-resources';
export * from './load-resources';
export * from './match-files';
export * from './normalize-options';
export * from './resolve-import-url';
export * from './type-guards';
export * from './validate-options';
+23
View File
@@ -0,0 +1,23 @@
import type {LoaderContext} from '../types';
import type {StyleResources, StyleResourcesLoaderNormalizedOptions} from '..';
import {errorMessage} from './error-message';
export const injectResources = async (
ctx: LoaderContext,
options: StyleResourcesLoaderNormalizedOptions,
source: string,
resources: StyleResources,
) => {
const {injector} = options;
const dist: unknown = injector.call(ctx, source, resources);
const content = await dist;
if (typeof content !== 'string') {
throw new Error(errorMessage.invalidInjectorReturn);
}
return content;
};
+19
View File
@@ -0,0 +1,19 @@
import type {LoaderContext, LoaderCallback} from '../types';
import {getResources} from './get-resources';
import {injectResources} from './inject-resources';
import {normalizeOptions} from './normalize-options';
export const loadResources = async (ctx: LoaderContext, source: string, callback: LoaderCallback) => {
try {
const options = normalizeOptions(ctx);
const resources = await getResources(ctx, options);
const content = await injectResources(ctx, options, source, resources);
callback(null, content);
} catch (err: unknown) {
callback(err as Error);
}
};
+48
View File
@@ -0,0 +1,48 @@
import path from 'path';
import util from 'util';
import glob from 'glob';
import type {LoaderContext, StyleResourcesLoaderNormalizedOptions} from '../types';
import {isStyleFile} from './type-guards';
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */
const isLegacyWebpack = (ctx: any): ctx is {options: {context: string}} => !!ctx.options;
const getRootContext = (ctx: LoaderContext) => {
/* istanbul ignore if: will be deprecated soon */
if (isLegacyWebpack(ctx)) {
return ctx.options.context;
}
return ctx.rootContext;
};
const flatten = <T>(items: T[][]) => {
const emptyItems: T[] = [];
return emptyItems.concat(...items);
};
export const matchFiles = async (ctx: LoaderContext, options: StyleResourcesLoaderNormalizedOptions) => {
const {patterns, globOptions} = options;
const files = await Promise.all(
patterns.map(async pattern => {
const rootContext = getRootContext(ctx);
const absolutePattern = path.isAbsolute(pattern) ? pattern : path.resolve(rootContext, pattern);
const partialFiles = await util.promisify(glob)(absolutePattern, globOptions);
return partialFiles.filter(isStyleFile);
}),
);
/**
* Glob always returns Unix-style file paths which would have cache invalidation problems on Windows.
* Use `path.resolve()` to convert Unix-style file paths to system-compatible ones.
*
* @see {@link https://github.com/yenshih/style-resources-loader/issues/17}
*/
return [...new Set(flatten(files))].map(file => path.resolve(file));
};
+46
View File
@@ -0,0 +1,46 @@
import {EOL} from 'os';
import {getOptions} from 'loader-utils';
import type {
LoaderContext,
StyleResource,
StyleResourcesNormalizedInjector,
StyleResourcesLoaderOptions,
StyleResourcesLoaderNormalizedOptions,
} from '../types';
import {validateOptions} from './validate-options';
const normalizePatterns = (patterns: StyleResourcesLoaderOptions['patterns']) =>
Array.isArray(patterns) ? patterns : [patterns];
const coerceContentEOL = (content: string) => (content.endsWith(EOL) ? content : `${content}${EOL}`);
const getResourceContent = ({content}: StyleResource) => coerceContentEOL(content);
const normalizeInjector = (injector: StyleResourcesLoaderOptions['injector']): StyleResourcesNormalizedInjector => {
if (typeof injector === 'undefined' || injector === 'prepend') {
return (source, resources) => resources.map(getResourceContent).join('') + source;
}
if (injector === 'append') {
return (source, resources) => source + resources.map(getResourceContent).join('');
}
return injector;
};
export const normalizeOptions = (ctx: LoaderContext): StyleResourcesLoaderNormalizedOptions => {
const options = getOptions(ctx);
validateOptions<StyleResourcesLoaderOptions>(options);
const {patterns, injector, globOptions = {}, resolveUrl = true} = options;
return {
patterns: normalizePatterns(patterns),
injector: normalizeInjector(injector),
globOptions,
resolveUrl,
};
};
+23
View File
@@ -0,0 +1,23 @@
import path from 'path';
import type {LoaderContext, StyleResource} from '../types';
/* eslint-disable-next-line prefer-named-capture-group */
const regex = /@(?:import|require)\s+(?:\([a-z,\s]+\)\s*)?['"]?([^'"\s;]+)['"]?;?/gu;
export const resolveImportUrl = (ctx: LoaderContext, {file, content}: StyleResource): StyleResource => ({
file,
content: content.replace(regex, (match: string, pathToResource?: string) => {
if (!pathToResource || /^[~/]/u.test(pathToResource)) {
return match;
}
const absolutePathToResource = path.resolve(path.dirname(file), pathToResource);
const relativePathFromContextToResource = path
.relative(ctx.context, absolutePathToResource)
.split(path.sep)
.join('/');
return match.replace(pathToResource, relativePathFromContextToResource);
}),
});
+7
View File
@@ -0,0 +1,7 @@
import path from 'path';
import {SUPPORTED_FILE_EXTS} from './constants';
export const isFunction = <T extends (...args: any[]) => any>(arg: any): arg is T => typeof arg === 'function';
export const isStyleFile = (file: string) => SUPPORTED_FILE_EXTS.includes(path.extname(file));
+11
View File
@@ -0,0 +1,11 @@
import validate from 'schema-utils';
import {schema} from '../schema';
import {LOADER_NAME, VALIDATION_BASE_DATA_PATH} from './constants';
export const validateOptions: <T extends object>(options: object) => asserts options is T = options =>
validate(schema, options, {
name: LOADER_NAME,
baseDataPath: VALIDATION_BASE_DATA_PATH,
});