From 525467c6b006cbae0097711de65b480d85dd0038 Mon Sep 17 00:00:00 2001 From: Andreas Ehrencrona Date: Fri, 11 Dec 2020 10:14:17 +0200 Subject: [PATCH 1/2] tests pass --- src/autoProcess.ts | 361 +++++++++++++++++-------- src/modules/tagInfo.ts | 60 +++- src/modules/utils.ts | 4 +- src/processors/typescript.ts | 29 +- src/transformers/typescript.ts | 5 +- src/types/index.ts | 2 + test/autoProcess/externalFiles.test.ts | 2 + 7 files changed, 335 insertions(+), 128 deletions(-) diff --git a/src/autoProcess.ts b/src/autoProcess.ts index 20400cad..2653fbd1 100644 --- a/src/autoProcess.ts +++ b/src/autoProcess.ts @@ -1,6 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + PreprocessorOptions, + SyncPreprocessor, +} from 'svelte/types/compiler/preprocess'; + import { PreprocessorGroup, - Preprocessor, Processed, TransformerArgs, TransformerOptions, @@ -8,7 +13,7 @@ import { Options, } from './types'; import { hasDepInstalled, concat } from './modules/utils'; -import { getTagInfo } from './modules/tagInfo'; +import { getTagInfoSync } from './modules/tagInfo'; import { addLanguageAlias, getLanguageFromAlias, @@ -56,13 +61,20 @@ type AutoPreprocessOptions = { [languageName: string]: TransformerOptions; }; -export const transform = async ( +type EventuallyProcessed = S extends true + ? Processed + : Processed | Promise; + +export const transform = ( name: string, options: TransformerOptions, { content, map, filename, attributes }: TransformerArgs, -): Promise => { + sync: S, +): EventuallyProcessed => { if (options === false) { - return { code: content }; + const res = { code: content }; + + return (sync ? res : Promise.resolve(res)) as any; } if (typeof options === 'function') { @@ -70,7 +82,17 @@ export const transform = async ( } // todo: maybe add a try-catch here looking for module-not-found errors - const { transformer } = await import(`./transformers/${name}`); + const { transformer, is_sync } = require(`./transformers/${name}`); + + if (sync && !is_sync) { + console.error(`Transfomer ${name} does not support sync calls.`) + + return { + code: content, + map, + dependencies: [], + } as EventuallyProcessed; + } return transformer({ content, @@ -81,6 +103,40 @@ export const transform = async ( }); }; +const pipe: ( + sync: S, + start: () => EventuallyProcessed, + ...then: Array<(processed: Processed) => EventuallyProcessed> +) => EventuallyProcessed = (sync, start, ...then) => + ((sync ? pipe_sync : pipe_async) as any)(start, ...then); + +async function pipe_async( + start: () => Promise, + ...then: Array<(processed: Processed) => Promise> +) { + let processed = await start(); + + for (let i = 0; i < then.length; i++) { + // eslint-disable-next-line no-await-in-loop + processed = await then[i](processed); + } + + return processed; +} + +function pipe_sync( + start: () => Processed, + ...then: Array<(processed: Processed) => Processed> +) { + let processed = start(); + + for (let i = 0; i < then.length; i++) { + processed = then[i](processed); + } + + return processed; +} + export function sveltePreprocess( { aliases, @@ -135,10 +191,11 @@ export function sveltePreprocess( return opts; }; - const getTransformerTo = ( + const getTransformerTo = ( type: 'markup' | 'script' | 'style', targetLanguage: string, - ): Preprocessor => async (svelteFile) => { + sync: S, + ) => (svelteFile): EventuallyProcessed => { let { content, filename, @@ -146,7 +203,7 @@ export function sveltePreprocess( alias, dependencies, attributes, - } = await getTagInfo(svelteFile); + } = getTagInfoSync(svelteFile); if (lang == null || alias == null) { alias = defaultLanguages[type]; @@ -154,7 +211,7 @@ export function sveltePreprocess( } if (preserve.includes(lang) || preserve.includes(alias)) { - return { code: content }; + return { code: content } as EventuallyProcessed; } const transformerOptions = getTransformerOptions(lang, alias); @@ -165,122 +222,206 @@ export function sveltePreprocess( }); if (lang === targetLanguage) { - return { code: content, dependencies }; + return { code: content, dependencies } as EventuallyProcessed; } - const transformed = await transform(lang, transformerOptions, { - content, - filename, - attributes, - }); - - return { - ...transformed, - dependencies: concat(dependencies, transformed.dependencies), - }; + return pipe( + sync, + () => + transform( + lang, + transformerOptions, + { + content, + filename, + attributes, + }, + sync, + ), + (processed) => + ({ + ...processed, + dependencies: concat(dependencies, processed.dependencies), + } as EventuallyProcessed), + ); }; - const scriptTransformer = getTransformerTo('script', 'javascript'); - const cssTransformer = getTransformerTo('style', 'css'); - const markupTransformer = getTransformerTo('markup', 'html'); - - const markup: PreprocessorGroup['markup'] = async ({ content, filename }) => { - if (transformers.replace) { - const transformed = await transform('replace', transformers.replace, { - content, - filename, - }); - - content = transformed.code; - } - - return transformMarkup({ content, filename }, markupTransformer, { - // we only pass the markupTagName because the rest of options - // is fetched internally by the `markupTransformer` - markupTagName, - }); + const scriptTransformer = getTransformerTo('script', 'javascript', false); + const scriptTransformerSync: SyncPreprocessor = getTransformerTo( + 'script', + 'javascript', + true, + ) as any; + + const cssTransformer = getTransformerTo('style', 'css', false); + const markupTransformer = getTransformerTo('markup', 'html', false); + + const markup: PreprocessorGroup['markup'] = ({ content, filename }) => { + return pipe( + false, + () => { + if (transformers.replace) { + return transform( + 'replace', + transformers.replace, + { + content, + filename, + }, + false, + ); + } + + return { code: content }; + }, + ({ code }) => { + return transformMarkup({ content: code, filename }, markupTransformer, { + // we only pass the markupTagName because the rest of options + // is fetched internally by the `markupTransformer` + markupTagName, + }); + }, + ); }; - const script: PreprocessorGroup['script'] = async ({ - content, - attributes, - filename, - }) => { - const transformResult: Processed = await scriptTransformer({ - content, - attributes, - filename, - }); - - let { code, map, dependencies, diagnostics } = transformResult; - - if (transformers.babel) { - const transformed = await transform( - 'babel', - getTransformerOptions('babel'), - { - content: code, - map, - filename, - attributes, + function buildScriptProcessor( + sync: S, + ): (options: PreprocessorOptions) => EventuallyProcessed { + return ({ content, attributes, filename }) => + pipe( + sync, + () => + getTransformerTo( + 'script', + 'javascript', + sync, + )({ + content, + attributes, + filename, + }), + ({ code, map, dependencies, diagnostics }) => { + if (transformers.babel) { + return pipe( + sync, + () => + transform( + 'babel', + getTransformerOptions('babel'), + { + content: code, + map, + filename, + attributes, + }, + sync, + ), + (transformed) => { + code = transformed.code; + map = transformed.map; + dependencies = concat(dependencies, transformed.dependencies); + diagnostics = concat(diagnostics, transformed.diagnostics); + + return { + code, + map, + dependencies, + diagnostics, + } as EventuallyProcessed; + }, + ); + } + + return { + code, + map, + dependencies, + diagnostics, + } as EventuallyProcessed; }, ); + } - code = transformed.code; - map = transformed.map; - dependencies = concat(dependencies, transformed.dependencies); - diagnostics = concat(diagnostics, transformed.diagnostics); - } - - return { code, map, dependencies, diagnostics }; - }; - - const style: PreprocessorGroup['style'] = async ({ - content, - attributes, - filename, - }) => { - const transformResult = await cssTransformer({ - content, - attributes, - filename, - }); - - let { code, map, dependencies } = transformResult; - - // istanbul ignore else - if (await hasDepInstalled('postcss')) { - if (transformers.postcss) { - const { alias } = getLanguage(attributes); - - const transformed = await transform( - 'postcss', - getTransformerOptions('postcss', alias), - { content: code, map, filename, attributes }, - ); - - code = transformed.code; - map = transformed.map; - dependencies = concat(dependencies, transformed.dependencies); - } - - const transformed = await transform( - 'globalStyle', - getTransformerOptions('globalStyle'), - { content: code, map, filename, attributes }, + const script: PreprocessorGroup['script'] = buildScriptProcessor(false); + const script_sync: PreprocessorGroup['script_sync'] = buildScriptProcessor< + true + >(true); + + function buildStyleProcessor( + sync: S, + ): (options: PreprocessorOptions) => EventuallyProcessed { + return ({ content, attributes, filename }) => { + const hasPostCss = hasDepInstalled('postcss'); + + return pipe( + sync, + () => + getTransformerTo( + 'style', + 'css', + sync, + )({ + content, + attributes, + filename, + }), + ({ code, map, dependencies }) => { + if (hasPostCss && transformers.postcss) { + const { alias } = getLanguage(attributes); + + return pipe( + sync, + () => + transform( + 'postcss', + getTransformerOptions('postcss', alias), + { content: code, map, filename, attributes }, + sync, + ), + (t) => + ({ + code: t.code, + map: t.map, + dependencies: concat(dependencies, t.dependencies), + } as EventuallyProcessed), + ); + } + + return { code, map, dependencies } as EventuallyProcessed; + }, + ({ code, map, dependencies }) => { + if (hasPostCss) { + return pipe( + sync, + () => + transform( + 'globalStyle', + getTransformerOptions('globalStyle'), + { content: code, map, filename, attributes }, + sync, + ), + (t) => + ({ + code: t.code, + map: t.map, + dependencies, + } as EventuallyProcessed), + ); + } + + return { code, map, dependencies } as EventuallyProcessed; + }, ); + }; + } - code = transformed.code; - map = transformed.map; - } - - return { code, map, dependencies }; - }; + const style: PreprocessorGroup['style'] = buildStyleProcessor(false); return { defaultLanguages, markup, script, + script_sync, style, }; } diff --git a/src/modules/tagInfo.ts b/src/modules/tagInfo.ts index ce9fcb07..da687afc 100644 --- a/src/modules/tagInfo.ts +++ b/src/modules/tagInfo.ts @@ -1,4 +1,4 @@ -import { readFile, access } from 'fs'; +import { readFile, access, accessSync, readFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { PreprocessorArgs } from '../types'; @@ -18,10 +18,68 @@ const getSrcContent = (file: string): Promise => { }); }; +const getSrcContentSync = (file: string) => { + const data = readFileSync(file); + + return data.toString(); +}; + async function doesFileExist(file: string) { return new Promise((resolve) => access(file, 0, (err) => resolve(!err))); } +function doesFileExistSync(file: string) { + try { + accessSync(file, 0); + + return true; + } catch (e) { + return false; + } +} + +export const getTagInfoSync = ({ + attributes, + filename, + content, +}: PreprocessorArgs) => { + const dependencies = []; + // catches empty content and self-closing tags + const isEmptyContent = content == null || content.trim().length === 0; + + /** only include src file if content of tag is empty */ + if (attributes.src && isEmptyContent) { + // istanbul ignore if + if (typeof attributes.src !== 'string') { + throw new Error('src attribute must be string'); + } + + let path = attributes.src; + + /** Only try to get local files (path starts with ./ or ../) */ + if (isValidLocalPath(path)) { + path = resolveSrc(filename, path); + if (doesFileExistSync(path)) { + content = getSrcContentSync(path); + dependencies.push(path); + } else { + console.warn(`[svelte-preprocess] The file "${path}" was not found.`); + } + } + } + + const { lang, alias } = getLanguage(attributes); + + return { + filename, + attributes, + content, + lang, + alias, + dependencies, + }; +}; + export const getTagInfo = async ({ attributes, filename, diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 0ce0dc9b..a952d4c4 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -39,7 +39,7 @@ const cachedResult: Record = {}; * @param {string} dep * @returns boolean */ -export async function hasDepInstalled(dep: string) { +export function hasDepInstalled(dep: string) { if (cachedResult[dep] != null) { return cachedResult[dep]; } @@ -47,7 +47,7 @@ export async function hasDepInstalled(dep: string) { let result = false; try { - await import(dep); + require(dep); result = true; } catch (e) { diff --git a/src/processors/typescript.ts b/src/processors/typescript.ts index 181baa73..d4615e29 100644 --- a/src/processors/typescript.ts +++ b/src/processors/typescript.ts @@ -1,18 +1,14 @@ import { Options, PreprocessorGroup } from '../types'; -import { getTagInfo } from '../modules/tagInfo'; +import { getTagInfoSync } from '../modules/tagInfo'; import { concat } from '../modules/utils'; import { prepareContent } from '../modules/prepareContent'; -export default (options?: Options.Typescript): PreprocessorGroup => ({ - async script(svelteFile) { - const { transformer } = await import('../transformers/typescript'); - let { - content, - filename, - attributes, - lang, - dependencies, - } = await getTagInfo(svelteFile); +export default (options?: Options.Typescript): PreprocessorGroup => { + const script = (svelteFile) => { + const { transformer } = require('../transformers/typescript'); + let { content, filename, attributes, lang, dependencies } = getTagInfoSync( + svelteFile, + ); content = prepareContent({ options, content }); @@ -20,7 +16,7 @@ export default (options?: Options.Typescript): PreprocessorGroup => ({ return { code: content }; } - const transformed = await transformer({ + const transformed = transformer({ content, filename, attributes, @@ -31,5 +27,10 @@ export default (options?: Options.Typescript): PreprocessorGroup => ({ ...transformed, dependencies: concat(dependencies, transformed.dependencies), }; - }, -}); + }; + + return { + script, + script_sync: script, + }; +}; diff --git a/src/transformers/typescript.ts b/src/transformers/typescript.ts index 74bcc9b9..f7a5456e 100644 --- a/src/transformers/typescript.ts +++ b/src/transformers/typescript.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import { dirname } from 'path'; import ts from 'typescript'; @@ -151,4 +152,6 @@ const transformer: Transformer = ({ }; }; -export { transformer }; +const is_sync = true; + +export { transformer, is_sync }; diff --git a/src/types/index.ts b/src/types/index.ts index 25ce8620..ea92740b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -34,6 +34,8 @@ export type Transformer = ( args: TransformerArgs, ) => Processed | Promise; +export type SyncTransformer = (args: TransformerArgs) => Processed; + export type TransformerOptions = boolean | T | Transformer; export interface Transformers { diff --git a/test/autoProcess/externalFiles.test.ts b/test/autoProcess/externalFiles.test.ts index c736575d..70f44ae9 100644 --- a/test/autoProcess/externalFiles.test.ts +++ b/test/autoProcess/externalFiles.test.ts @@ -32,6 +32,7 @@ describe('external files', () => { `, filename: resolve(__dirname, '..', 'App.svelte'), + attributes: {}, }), await scriptProcessor({ content: ``, @@ -57,6 +58,7 @@ describe('external files', () => { const markup = await markupProcessor({ content: `