This commit is contained in:
darenhsu
2022-07-17 13:16:16 +08:00
parent 84759556ff
commit befd344ab0
28070 changed files with 4008428 additions and 1 deletions
+6
View File
@@ -0,0 +1,6 @@
import loader from './loader';
export * from './schema';
export * from './types';
export default loader;
+25
View File
@@ -0,0 +1,25 @@
import {errorMessage, isFunction, loadResources} from './utils';
import {Loader, LoaderCallback} from '.';
/* eslint-disable no-invalid-this */
const loader: Loader = function(source) {
this.cacheable && 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);
}
/* eslint-disable-next-line @typescript-eslint/no-floating-promises */
loadResources(this, source, callback);
};
/* eslint-enable no-invalid-this */
export default loader;
+40
View File
@@ -0,0 +1,40 @@
import 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,
};
+35
View File
@@ -0,0 +1,35 @@
import {loader} from 'webpack';
import 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 = (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 {StyleResourcesFileFormat} from '..';
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 '.';
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,
);
+25
View File
@@ -0,0 +1,25 @@
import fs from 'fs';
import util from 'util';
import {LoaderContext, StyleResource, StyleResourcesLoaderNormalizedOptions} from '..';
import {matchFiles, resolveImportUrl} from '.';
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';
+21
View File
@@ -0,0 +1,21 @@
import {StyleResources, StyleResourcesLoaderNormalizedOptions} from '..';
import {isPromise, errorMessage} from '.';
export const injectResources = async (
options: StyleResourcesLoaderNormalizedOptions,
source: string,
resources: StyleResources,
) => {
const {injector} = options;
const dist: any = injector(source, resources);
const content = isPromise(dist) ? await dist : dist;
if (typeof content !== 'string') {
throw new Error(errorMessage.invalidInjectorReturn);
}
return content;
};
+17
View File
@@ -0,0 +1,17 @@
import {LoaderContext, LoaderCallback} from '..';
import {normalizeOptions, getResources, injectResources} from '.';
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(options, source, resources);
return callback(null, content);
} catch (err) {
return callback(err);
}
};
+47
View File
@@ -0,0 +1,47 @@
import path from 'path';
import util from 'util';
import glob from 'glob';
import {LoaderContext, StyleResourcesLoaderNormalizedOptions} from '..';
import {isStyleFile} from '.';
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 {
LoaderContext,
StyleResource,
StyleResourcesNormalizedInjector,
StyleResourcesLoaderOptions,
StyleResourcesLoaderNormalizedOptions,
} from '..';
import {validateOptions} from '.';
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 {LoaderContext, StyleResource} from '..';
/* 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);
}),
});
+11
View File
@@ -0,0 +1,11 @@
import path from 'path';
import isPromise from 'is-promise';
import {SUPPORTED_FILE_EXTS} from '.';
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));
export {isPromise};
+11
View File
@@ -0,0 +1,11 @@
import validate from 'schema-utils';
import {schema} from '..';
import {LOADER_NAME, VALIDATION_BASE_DATA_PATH} from '.';
export const validateOptions: <T extends {}>(options: any) => asserts options is T = options =>
validate(schema, options, {
name: LOADER_NAME,
baseDataPath: VALIDATION_BASE_DATA_PATH,
});