From f2add6c21fe7b4a0774412f4aab5c0860b3b08c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Veyret?= Date: Tue, 14 Jan 2020 13:48:25 +0100 Subject: [PATCH 1/2] Update dependencies --- .gitignore | 3 ++- .nycrc.yaml | 10 ++++++++++ package.json | 30 ++++++++---------------------- src/AssetModuleManager.ts | 1 + src/DeclarationNodeFinder.ts | 1 + 5 files changed, 22 insertions(+), 23 deletions(-) create mode 100644 .nycrc.yaml diff --git a/.gitignore b/.gitignore index ef801a4..445d575 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,8 @@ test/*.js .nyc_output/ coverage/ -# Yarn +# Npm and Yarn +package-lock.json yarn-error.log yarn.lock diff --git a/.nycrc.yaml b/.nycrc.yaml new file mode 100644 index 0000000..1f15fb3 --- /dev/null +++ b/.nycrc.yaml @@ -0,0 +1,10 @@ +--- +extends: '@istanbuljs/nyc-config-typescript' +include: + - 'src/**' +exclude: + - '**/*.spec.ts' +reporter: + - lcov + - text +all: true diff --git a/package.json b/package.json index bee9d79..be90193 100644 --- a/package.json +++ b/package.json @@ -20,22 +20,6 @@ "debug": "npm run debug:watch", "debug:watch": "npm-watch test" }, - "nyc": { - "extension": [ - ".ts" - ], - "include": [ - "src/**" - ], - "exclude": [ - "**/*.spec.ts" - ], - "reporter": [ - "lcov", - "text" - ], - "all": true - }, "keywords": [ "typescript", "transform", @@ -57,15 +41,17 @@ "typescript": "^3.0.0" }, "devDependencies": { - "@types/chai": "4.2.6", + "@istanbuljs/nyc-config-typescript": "1.0.1", + "@types/chai": "4.2.7", "@types/mocha": "5.2.7", - "@types/node": "12.12.16", + "@types/node": "13.1.6", "chai": "4.2.0", - "mocha": "6.2.2", - "nyc": "14.1.1", + "mocha": "7.0.0", + "nyc": "15.0.0", "rimraf": "3.0.0", - "ts-node": "8.5.4", + "source-map-support": "0.5.16", + "ts-node": "8.6.2", "tslint": "5.20.1", - "typescript": "3.7.3" + "typescript": "3.7.4" } } diff --git a/src/AssetModuleManager.ts b/src/AssetModuleManager.ts index 1cc83c3..6bbf5d1 100644 --- a/src/AssetModuleManager.ts +++ b/src/AssetModuleManager.ts @@ -22,6 +22,7 @@ export default class AssetModuleManager { * @returns The build module name or undefined if not matching. */ public buildName(moduleSpecifier?: Expression): string | undefined { + /* istanbul ignore else */ if (moduleSpecifier) { // Remove quotes for name let moduleName: string = moduleSpecifier.getText() diff --git a/src/DeclarationNodeFinder.ts b/src/DeclarationNodeFinder.ts index 09e4160..6566dc8 100644 --- a/src/DeclarationNodeFinder.ts +++ b/src/DeclarationNodeFinder.ts @@ -21,6 +21,7 @@ export default class DeclarationNodeFinder { const symbol = this.typeChecker.getSymbolAtLocation(node) if (symbol) { const declarations = symbol.getDeclarations() + /* istanbul ignore else */ if (declarations && declarations.length === 1) { return declarations[0] } From 95cb19ed746fbc6c5580b41619d3eb012d1d7a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Veyret?= Date: Tue, 14 Jan 2020 16:54:11 +0100 Subject: [PATCH 2/2] Add ability to give hash and more to asset names --- README.md | 31 +++++++++-- doc/fr/README.md | 31 +++++++++-- package.json | 10 ++-- src/AssetModuleManager.ts | 71 ++++++++++++++++++++++--- src/PluginConfig.ts | 2 +- src/compile.spec.ts | 6 ++- src/index.spec.ts | 107 +++++++++++++++++++++++++++++++------- src/index.ts | 15 ++++-- test/image.svg | 8 +++ test/reexport.ts | 2 +- test/success.ts | 12 +++-- 11 files changed, 243 insertions(+), 52 deletions(-) create mode 100644 test/image.svg diff --git a/README.md b/README.md index b5b33d0..e79c33a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ export const foobar = 'assets/foobar.ico' # Language/langue -Because French is my native language, finding all documents and messages in French is not an option. Other translations are welcome. +Because French is my native language, finding all documents and messages in French is mandatory. Other translations are welcome. Anyway, because English is the language of programming, the code, including variable names and comments, are in English. @@ -62,7 +62,7 @@ Using this transformer to transpile the web pages (not for `Webpack`!), you will The transformer accepts the following parameters: - `assetsMatch`: a regular expression used to select asset imports, e.g., for all `.png` files, `assetsMatch = "\\.png$"`. This parameter is mandatory. -- `targetPath`: a path which is prefixed to the file name, i.e. the same as the `publicPath` you may have defined in the `output` parameter of `Webpack`. This parameter is optional. +- `targetName`: a template similar to [Webpack file-loader name](https://webpack.js.org/loaders/file-loader/#name) used to convert the name of the asset. If you defined a `publicPath` in the `output` parameter of `Webpack`, then you will probably need to specify this path here too. This parameter is optional and defaults to `[hash].[ext]`. There is currently no way of declaring a transformer in the vanilla `typescript` compiler. If you do not want to write your own compiler using the `typescript` API, you can use the `ttypescript` wrapper. Below is explained how. @@ -91,7 +91,7 @@ Then, configure your `tsconfig.json` { "transform": "ts-transform-asset", "assetsMatch": "\\.png$", - "targetPath": "assets" + "targetName": "assets/[name]-[hash].[ext]" } ] } @@ -163,4 +163,27 @@ const url: string = image # Migration -Note that in versions 1.x.x, transformer was of `config` type. Since version 2.0.0, transformer is of `program` type, which is the default. If you are upgrading from an older version and using `ttypescript`, you have to update the `plugin` configuration in `tsconfig.json`. +## Prior to version 3.x.x + +Prior to version 3.x.x, there was a configuration entry `targetPath` which was the prefix used to add to the target asset name. Everything is now defined in the new `targetName` entry. Converting from previous to current configuration is as simple as the below example: + +```diff + "transform": "ts-transform-asset", + "assetsMatch": "\\.png$", +- "targetPath": "assets" ++ "targetName": "assets/[name].[ext]" + } + ] +``` + +## Prior to version 2.x.x + +In addition to the previous modifications, note that prior to version 2.x.x, transformer was of type `config`. Since version 2.0.0, transformer is of type `program`, which is the default. If you are upgrading from an older version and using `ttypescript`, you have to update the `plugin` configuration in `tsconfig.json`: + +```diff + { + "transform": "ts-transform-asset", +- "type": "config", + "assetsMatch": "\\.png$", + "targetName": "assets/[name].[ext]" +``` diff --git a/doc/fr/README.md b/doc/fr/README.md index 1e842fa..e19d784 100644 --- a/doc/fr/README.md +++ b/doc/fr/README.md @@ -21,7 +21,7 @@ export const foobar = 'assets/foobar.ico' # Langue -Le français étant ma langue maternelle, fournir les documents et messages en français n'est pas une option. Les autres traductions sont bienvenues. +Le français étant ma langue maternelle, fournir les documents et messages en français est obligatoire. Les autres traductions sont bienvenues. Cependant, l'anglais étant la langue de la programmation, le code, y compris les noms de variable et commentaires, sont en anglais. @@ -54,7 +54,7 @@ L'utilisation de ce transformateur pour transpiler les pages web (pas pour `Webp Le transformateur accepte les paramètres suivants : - `assetsMatch`: une expression rationnelle utilisée pour sélectionner les imports de fichiers annexes, par exemple, pour tous les fichiers `.png`, `assetsMatch = "\\.png$"` — ce paramètre est obligatoire; -- `targetPath`: un chemin qui est préfixé au nom de fichier, c'est le `publicPath` que vous pouvez avoir défini dans le paramètre `output` de `Webpack` — ce paramètre est optionnel. +- `targetName`: un patron similaire au [Webpack file-loader name](https://webpack.js.org/loaders/file-loader/#name) utilisé pour convertir le nom du fichier annexe — si vous définissez un `publicPath` dans le paramètre `output` de `Webpack`, vous aurez probablement besoin de spécifier ce chemin ici également — ce paramètre est optionnel et vaut `[hash].[ext]` par défaut. Il n'y a actuellement pas moyen de déclarer un transformateur dans le compilateur `typescript` standard. Si vous ne souhaitez pas écrire votre propre compilateur en utilisant l'API `typescript`, vous pouvez utiliser la surcouche `ttypescript`. Les explications sont données ci-dessous. @@ -83,7 +83,7 @@ Ensuite, configurez votre `tsconfig.json` { "transform": "ts-transform-asset", "assetsMatch": "\\.png$", - "targetPath": "assets" + "targetName": "assets/[name]-[hash].[ext]" } ] } @@ -155,4 +155,27 @@ const url: string = image # Migration -Notez que dans les versions 1.x.x, le transformateur était de type `config`. Depuis la version 2.0.0, le transformateur est de type `program`, qui est le type par défaut. Si vous mettez à jour depuis une version plus ancienne et que vous utilisez `ttypescript`, vous devrez mettre à jour la configuration du `plugin` dans `tsconfig.json`. +## Versions antérieures à 3.x.x + +Avant la version 3.x.x, il y avait une entrée de configuration nommée `targetPath` qui définissait un chemin préfixé au nom de fichier. Tout est maintenant défini par la nouvelle entrée `targetName`. La conversion d'une ancienne configuration vers la nouvelle est aussi simple que l'exemple ci-dessous : + +```diff + "transform": "ts-transform-asset", + "assetsMatch": "\\.png$", +- "targetPath": "assets" ++ "targetName": "assets/[name].[ext]" + } + ] +``` + +## Versions antérieur à 2.x.x + +En plus des modifications précédentes, notez qu'avant la version 2.x.x, le transformateur était de type `config`. Depuis la version 2.0.0, le transformateur est de type `program`, qui est le type par défaut. Si vous mettez à jour depuis une version plus ancienne et que vous utilisez `ttypescript`, vous devrez mettre à jour la configuration du `plugin` dans `tsconfig.json` : + +```diff + { + "transform": "ts-transform-asset", +- "type": "config", + "assetsMatch": "\\.png$", + "targetName": "assets/[name].[ext]" +``` diff --git a/package.json b/package.json index be90193..3229bd6 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "ts-transform-asset", - "version": "2.0.0", + "version": "3.0.0", "description": "Typescript transformer used to convert asset imports to file names", "main": "dist/index.js", "typings": "dist/index.d.ts", "repository": { "type": "git", - "url": "git+https://github.com/sveyret/ts-transform-asset.git" + "url": "git+https://github.com/slune-org/ts-transform-asset.git" }, "scripts": { "prepublishOnly": "npm run all", @@ -24,9 +24,11 @@ "typescript", "transform", "asset", - "filename" + "filename", + "webpack", + "file-loader" ], - "author": "Stéphane Veyret", + "author": "Slune", "license": "MIT", "watch": { "test": { diff --git a/src/AssetModuleManager.ts b/src/AssetModuleManager.ts index 6bbf5d1..49946de 100644 --- a/src/AssetModuleManager.ts +++ b/src/AssetModuleManager.ts @@ -1,4 +1,6 @@ -import { basename, join } from 'path' +import { HexBase64Latin1Encoding, createHash } from 'crypto' +import { existsSync, readFileSync } from 'fs' +import { basename, dirname, join, parse, relative, sep } from 'path' import { Expression } from 'typescript' /** @@ -6,13 +8,27 @@ import { Expression } from 'typescript' * specifier. */ export default class AssetModuleManager { + /** + * The directory of the current file, root of module search. + */ + private currentPath: string + /** * Create the object. * * @param assetsMatch - The regular expression for detecting matching modules. - * @param targetPath - The public target path for the assets. + * @param targetName - The public target name to use for the assets. + * @param filename - The name of the file currently being transformed. + * @param basePath - The base path of the project. */ - public constructor(private assetsMatch: RegExp, private targetPath?: string) {} + public constructor( + private assetsMatch: RegExp, + private targetName: string, + filename: string, + private basePath: string + ) { + this.currentPath = dirname(filename) + } /** * Build the module name as it should be used inside source file, if the module specifier matches the @@ -30,12 +46,53 @@ export default class AssetModuleManager { // Check if matching assets pattern if (this.assetsMatch.test(moduleName)) { - // Convert file name - moduleName = basename(moduleName) - this.targetPath && (moduleName = join(this.targetPath, moduleName)) - return moduleName + return this.interpolateName(moduleName) } } return undefined } + + /** + * Create the asset name using `targetName` template and given module name. + * @param moduleName The name of module to use as interpolation source. + */ + private interpolateName(moduleName: string) { + const modulePath = join(this.currentPath, moduleName) + const parsed = parse(modulePath) + /* istanbul ignore next */ + const ext = parsed.ext ? parsed.ext.substr(1) : 'bin' + /* istanbul ignore next */ + const filename = parsed.name || 'file' + + let directory = + relative(this.basePath, parsed.dir) + .replace(/\\/g, '/') + .replace(/\.\.(\/)?/g, '_$1') + sep + let folder = '' + if (directory.length === 1) { + directory = '' + } else { + folder = basename(directory) + } + + let url = this.targetName + if (existsSync(modulePath)) { + const content = readFileSync(modulePath) + url = url.replace( + /\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi, + (_, hashType: string, digestType: HexBase64Latin1Encoding | '', maxLength: string) => { + const hash = createHash(hashType || 'md5') + hash.update(content) + return hash.digest(digestType || 'hex').substr(0, parseInt(maxLength, 10) || 9999) + } + ) + } + + url = url + .replace(/\[ext\]/gi, ext) + .replace(/\[name\]/gi, filename) + .replace(/\[path\]/gi, directory) + .replace(/\[folder\]/gi, folder) + return url + } } diff --git a/src/PluginConfig.ts b/src/PluginConfig.ts index c4b1809..c34dbee 100644 --- a/src/PluginConfig.ts +++ b/src/PluginConfig.ts @@ -1,4 +1,4 @@ export default interface PluginConfig { assetsMatch: string - targetPath?: string + targetName?: string } diff --git a/src/compile.spec.ts b/src/compile.spec.ts index 6a43009..c4021e5 100644 --- a/src/compile.spec.ts +++ b/src/compile.spec.ts @@ -24,19 +24,21 @@ const TS_CONFIG: CompilerOptions = { export default function compile( testName: string, + rootDir: string | undefined, input: string[], assetsMatch: string, - targetPath?: string + targetName?: string ): void { const options: CompilerOptions = { ...TS_CONFIG, + rootDir, outDir: `dist/test/${testName}`, } const compilerHost = createCompilerHost(options) const program = createProgram(input, options, compilerHost) const emitResult = program.emit(undefined, undefined, undefined, undefined, { - before: [transform(program, { assetsMatch, targetPath })], + before: [transform(program, { assetsMatch, targetName })], }) const allDiagnostics = getPreEmitDiagnostics(program).concat(emitResult.diagnostics) diff --git a/src/index.spec.ts b/src/index.spec.ts index 8f45780..6d936fa 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,6 +1,4 @@ -// tslint:disable:only-arrow-functions (mocha prefers functions) -// tslint:disable:no-unused-expression (chai expression are actually used) -// tslint:disable:no-implicit-dependencies (dev deps are enough for tests) +// tslint:disable import { expect } from 'chai' import { resolve } from 'path' @@ -10,27 +8,96 @@ describe('ts-transform-asset', function() { this.slow(2000) this.timeout(10000) - function compileFile(testName: string, path?: string): void { + function compileFile(testName: string, rootDir: string | undefined, targetName?: string): void { const files: string[] = [resolve(__dirname, '../test/global.d.ts')] files.push(...['success', 'failure', 'reexport'].map(name => resolve(__dirname, `../test/${name}.ts`))) - compile(testName, files, '\\.(png|svg|ogg)$', path) + compile(testName, rootDir, files, '\\.(png|svg|ogg)$', targetName) } - it('should be able to compile asset to root path', function() { - compileFile('root') - expect(require('../dist/test/root/success').default('png')).to.equal('image.png') - expect(require('../dist/test/root/success').default('svg')).to.equal('image.svg') - expect(require('../dist/test/root/success').default('defaultExport')).to.equal('image.svg') - expect(require('../dist/test/root/success').default('export')).to.equal('image.svg') - expect(() => require('../dist/test/root/failure')).to.throw() - }) + ;[ + { + name: 'root', + template: '[name].[ext]', + result: { + fullImport: 'image.png', + defaultImport: 'image.svg', + defaultExport: 'image.svg', + namedExport: 'image.svg', + }, + }, + { + name: 'path', + template: 'assets/[name].[ext]', + result: { + fullImport: 'assets/image.png', + defaultImport: 'assets/image.svg', + defaultExport: 'assets/image.svg', + namedExport: 'assets/image.svg', + }, + }, + { + name: 'default', + result: { + fullImport: '[hash].png', + defaultImport: 'b05767c238cb9f989cf3cd8180594878.svg', + defaultExport: 'b05767c238cb9f989cf3cd8180594878.svg', + namedExport: 'b05767c238cb9f989cf3cd8180594878.svg', + }, + }, + { + name: 'format', + rootDir: 'test', + template: '[path][folder]_[hash]-[contenthash].[ext]', + result: { + fullImport: 'sub/folder/folder_[hash]-[contenthash].png', + defaultImport: '_b05767c238cb9f989cf3cd8180594878-b05767c238cb9f989cf3cd8180594878.svg', + defaultExport: '_b05767c238cb9f989cf3cd8180594878-b05767c238cb9f989cf3cd8180594878.svg', + namedExport: '_b05767c238cb9f989cf3cd8180594878-b05767c238cb9f989cf3cd8180594878.svg', + }, + }, + { + name: 'formatNoRoot', + template: '[path][folder]_[hash]-[contenthash].[ext]', + result: { + fullImport: 'test/sub/folder/folder_[hash]-[contenthash].png', + defaultImport: 'test/test_b05767c238cb9f989cf3cd8180594878-b05767c238cb9f989cf3cd8180594878.svg', + defaultExport: 'test/test_b05767c238cb9f989cf3cd8180594878-b05767c238cb9f989cf3cd8180594878.svg', + namedExport: 'test/test_b05767c238cb9f989cf3cd8180594878-b05767c238cb9f989cf3cd8180594878.svg', + }, + }, + ].forEach(testCase => { + describe(`Compile with ${testCase.template || 'default'} template`, function() { + before(`Compile files to ${testCase.name}`, function() { + compileFile(testCase.name, testCase.rootDir, testCase.template) + }) + + it('should find full module import file', function() { + expect(require(`../dist/test/${testCase.name}/success`).default('fullImport')).to.equal( + testCase.result.fullImport + ) + }) + + it('should find default module import file', function() { + expect(require(`../dist/test/${testCase.name}/success`).default('defaultImport')).to.equal( + testCase.result.defaultImport + ) + }) + + it('should find default re-exported file', function() { + expect(require(`../dist/test/${testCase.name}/success`).default('defaultExport')).to.equal( + testCase.result.defaultExport + ) + }) + + it('should find named re-exported file', function() { + expect(require(`../dist/test/${testCase.name}/success`).default('namedExport')).to.equal( + testCase.result.namedExport + ) + }) - it('should be able to compile asset to given path', function() { - compileFile('path', 'assets') - expect(require('../dist/test/path/success').default('png')).to.equal('assets/image.png') - expect(require('../dist/test/path/success').default('svg')).to.equal('assets/image.svg') - expect(require('../dist/test/path/success').default('defaultExport')).to.equal('assets/image.svg') - expect(require('../dist/test/path/success').default('export')).to.equal('assets/image.svg') - expect(() => require('../dist/test/path/failure')).to.throw() + it('should fail to require bad module', function() { + expect(() => require(`../dist/test/${testCase.name}/failure`)).to.throw() + }) + }) }) }) diff --git a/src/index.ts b/src/index.ts index 3d36e59..b87be9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,11 +23,13 @@ function buildVisitor( typeChecker: TypeChecker, ctx: TransformationContext, assetsMatch: RegExp, - targetPath?: string + targetName: string, + filename: string, + basePath: string ) { const modifiedImports: Node[] = [] const declarationNode = new DeclarationNodeFinder(typeChecker) - const moduleManager = new AssetModuleManager(assetsMatch, targetPath) + const moduleManager = new AssetModuleManager(assetsMatch, targetName, filename, basePath) const allVisitors: Array> = [ new ImportVisitor(declarationNode, moduleManager, modifiedImports), new ExportVisitor(moduleManager), @@ -64,7 +66,12 @@ function buildVisitor( } export default function(program: Program, pluginConfig: PluginConfig): TransformerFactory { - const assetsMatch: RegExp = new RegExp(pluginConfig.assetsMatch) + const assetsMatch = new RegExp(pluginConfig.assetsMatch) + const targetName = pluginConfig.targetName || '[hash].[ext]' + const basePath = program.getCompilerOptions().rootDir || program.getCurrentDirectory() return (ctx: TransformationContext): Transformer => (sf: SourceFile) => - visitNode(sf, buildVisitor(program.getTypeChecker(), ctx, assetsMatch, pluginConfig.targetPath)) + visitNode( + sf, + buildVisitor(program.getTypeChecker(), ctx, assetsMatch, targetName, sf.fileName, basePath) + ) } diff --git a/test/image.svg b/test/image.svg new file mode 100644 index 0000000..7924efc --- /dev/null +++ b/test/image.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/test/reexport.ts b/test/reexport.ts index 99b9f13..124a5d3 100644 --- a/test/reexport.ts +++ b/test/reexport.ts @@ -1 +1 @@ -export { default, default as image } from './image.svg' +export { default, default as image } from 'image.svg' diff --git a/test/success.ts b/test/success.ts index 25a8369..520dd32 100644 --- a/test/success.ts +++ b/test/success.ts @@ -1,17 +1,19 @@ -import * as pngImage from './image.png' +import * as pngImage from './sub/folder/image.png' import svgImage from './image.svg' import defaultImage from './reexport' import { image } from './reexport' -export default function getPath(type: 'png' | 'svg' | 'defaultExport' | 'export'): string { +export default function getPath( + type: 'fullImport' | 'defaultImport' | 'defaultExport' | 'namedExport' +): string { switch (type) { - case 'png': + case 'fullImport': return pngImage - case 'svg': + case 'defaultImport': return svgImage case 'defaultExport': return defaultImage - case 'export': + case 'namedExport': return image } }