Skip to content

Commit 91e19d2

Browse files
committed
simple css asset import bundling
1 parent 32783f7 commit 91e19d2

File tree

7 files changed

+176
-7
lines changed

7 files changed

+176
-7
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "typescript-bundle",
3-
"version": "1.0.6",
3+
"version": "1.0.7",
44
"description": " A zero configuration bundling tool for TypeScript.",
55
"scripts": {
66
"clean": "node tasks clean",

readme.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,14 @@ $ tsc-bundle ./src/tsconfig.json --outFile ./bundle.js
100100
```typescript
101101
import Content from 'text!./file.txt'
102102
```
103-
TypeScript-Bundle can automatically bundle files with a special import scheme similar to WebPack's [ts-loader](https://github.com/TypeStrong/ts-loader). It supports `text`, `json`, `base64`, and `buffer` directives that inform the bundler how to embed the asset.
103+
TypeScript-Bundle automatically bundles files with a special import scheme similar to WebPack's [ts-loader](https://github.com/TypeStrong/ts-loader). It supports `text`, `json`, `base64`, `buffer` and `css` directives that inform the bundler how to embed the asset.
104104

105105
```typescript
106106
import Text from 'text!./file.txt' // as 'string'
107107
import Base64 from 'base64!./image.png' // as 'string | base64 encoded'
108108
import Obj from 'json!./file.json' // as 'any'
109109
import Buf from 'buffer!./file.dat' // as 'Uint8Array'
110+
import Css from 'css!./file.css' // as 'string | @import concat'
110111
```
111112
112113
### Declarations
@@ -129,6 +130,10 @@ declare module '*.buf' {
129130
const value: Uint8Array
130131
export default value
131132
}
133+
declare module '*.css' {
134+
const value: string
135+
export default value
136+
}
132137
```
133138
You can either add this declaration to your `tsconfig.json` or as a `/// <reference path='extensions.d.ts' />` directive if bundling from script.
134139

src/bundler/resources/resources.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { asBase64 } from './templates/index'
3030
import { asBuffer } from './templates/index'
3131
import { asJson } from './templates/index'
3232
import { asText } from './templates/index'
33+
import { asCss } from './templates/index'
3334
import { join, dirname } from 'path'
3435

3536
// ------------------------------------------------------------------------------
@@ -48,7 +49,7 @@ import { join, dirname } from 'path'
4849
//
4950
// ------------------------------------------------------------------------------
5051

51-
type Directive = 'text' | 'json' | 'base64' | 'buffer'
52+
type Directive = 'text' | 'json' | 'base64' | 'buffer' | 'css'
5253
type Define = Module | Resource
5354
interface Module {
5455
type: 'module'
@@ -86,7 +87,12 @@ class AMDReader {
8687
const dependencies = unmapped.map(dependency => {
8788
if(dependency.includes('!')) {
8889
const split = dependency.split('!').map(n => n.trim())
89-
if(split.length === 2 && (split[0] === 'text' || split[0] === 'json' || split[0] === 'base64' || split[0] === 'buffer')) {
90+
if(split.length === 2 &&
91+
(split[0] === 'text' ||
92+
split[0] === 'json' ||
93+
split[0] === 'base64' ||
94+
split[0] === 'buffer' ||
95+
split[0] === 'css')) {
9096
const directive = split[0]
9197
const absoluteName = join(module_root, split[1])
9298
const resource = `${directive}!${absoluteName}`
@@ -149,7 +155,7 @@ export class Resources {
149155
// Rewrites Module Resource Dependencies
150156
for(const key of Object.keys(result.remaps)) {
151157
for(const dependency of result.remaps[key]) {
152-
const pattern = new RegExp(dependency)
158+
const pattern = new RegExp(dependency, 'g')
153159
code = code.replace(pattern, key)
154160
}
155161
}
@@ -162,6 +168,7 @@ export class Resources {
162168
case 'json': return asJson(resource.name, path)
163169
case 'base64': return asBase64(resource.name, path)
164170
case 'buffer': return asBuffer(resource.name, path)
171+
case 'css': return asCss(resource.name, path)
165172
default: throw Error(`unknown directive '${resource.directive}'`)
166173
}
167174
})
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*--------------------------------------------------------------------------
2+
3+
typescript-bundle
4+
5+
The MIT License (MIT)
6+
7+
Copyright (c) 2019 Haydn Paterson (sinclair) <haydn.developer@gmail.com>
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in
17+
all copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
27+
---------------------------------------------------------------------------*/
28+
29+
import { resolve, join, dirname } from 'path'
30+
import { readFileSync, existsSync } from 'fs'
31+
import { render } from './render'
32+
33+
// ------------------------------------------------------------------------------
34+
//
35+
// CssReader
36+
//
37+
// Reads Css files and resolves their internal @imports. Detects both
38+
// 'file-not-found' and 'cyclic import' errors and throws in either
39+
// case.
40+
//
41+
// ------------------------------------------------------------------------------
42+
43+
class CssFileNotFoundError extends Error {
44+
constructor(filePath: string) {
45+
super(`bundler: css file '${filePath}' not found.`)
46+
}
47+
}
48+
49+
class CssCyclicReadError extends Error {
50+
constructor(stack: string[]) {
51+
super('')
52+
const buffer = ['bundler: cyclic css @import detected.']
53+
stack.forEach((item, index) => {
54+
const padding = Array.from({length: index + 1}).join('x').replace(/x/g, ' ')
55+
buffer.push(`${padding}${item}`)
56+
})
57+
this.message = buffer.join('\n')
58+
}
59+
}
60+
61+
interface CssFile {
62+
filePath: string
63+
content: string
64+
includes: string[]
65+
}
66+
67+
class CssReader {
68+
private resolving: {[key: string]: string }
69+
private resolved: {[key: string]: CssFile }
70+
private filePath: string
71+
constructor(filePath: string) {
72+
this.filePath = resolve(filePath)
73+
this.resolved = {}
74+
this.resolving = {}
75+
}
76+
77+
/** Parses the contents of the CSS file. */
78+
private parseCssFile(filePath: string): CssFile {
79+
filePath = resolve(filePath)
80+
const imports = [] as {includePattern: string, includePath: string}[]
81+
if(!existsSync(filePath)) {
82+
throw new CssFileNotFoundError(filePath)
83+
}
84+
const contentRaw = readFileSync(filePath, 'utf8')
85+
const pattern = /@import\s*["'](.*)["'];?/g
86+
// Parse @import's from document
87+
while(true) {
88+
const result = pattern.exec(contentRaw)
89+
if(!result) {
90+
break
91+
}
92+
const includePattern = result[0]
93+
const includePathRaw = result[1]
94+
const dirPath = dirname(filePath)
95+
const includePath = resolve(join(dirPath, includePathRaw))
96+
imports.push({includePattern, includePath})
97+
}
98+
99+
// Build includes array and replace @import strings.
100+
const includes = imports.map(_import => _import.includePath)
101+
const content = imports.reduce((content, include) => {
102+
return content.replace(new RegExp(include.includePattern, 'g'), '')
103+
}, contentRaw)
104+
return { filePath, content, includes }
105+
}
106+
107+
/** Loads file recursively and store in resolved cache. */
108+
private loadCssFile(filePath: string) {
109+
filePath = resolve(filePath)
110+
if(this.resolved[filePath]) {
111+
return
112+
}
113+
if(this.resolving[filePath]) {
114+
const stack = [...Object.keys(this.resolving), filePath]
115+
throw new CssCyclicReadError(stack)
116+
}
117+
this.resolving[filePath] = filePath
118+
const file = this.parseCssFile(filePath)
119+
for(const include of file.includes) {
120+
this.loadCssFile(include)
121+
}
122+
delete this.resolving[filePath]
123+
this.resolved[filePath] = file
124+
}
125+
126+
/** Reads the CSS document concats its imports. */
127+
public read(): string {
128+
this.loadCssFile(this.filePath)
129+
return Object.keys(this.resolved)
130+
.map(key => this.resolved[key]!.content)
131+
.join('\n')
132+
}
133+
}
134+
135+
function get(filePath: string): string {
136+
try {
137+
const reader = new CssReader(filePath)
138+
const content = reader.read()
139+
const lines = content.split('\n').map(line => {
140+
const text = line
141+
.replace(/\r/g, '')
142+
.replace(/\\/g, '\\\\"')
143+
.replace(/"/g, '\\"')
144+
return ` "${text}"`
145+
}).join(',\n')
146+
return `[\n${lines}\n ].join('\\n')`
147+
} catch(error) {
148+
// todo: consider better ways to notify error.
149+
console.log(error.message + '\n')
150+
return '""'
151+
}
152+
}
153+
154+
export function asCss(moduleName: string, filePath: string) {
155+
return render(moduleName, get(filePath))
156+
}

src/bundler/resources/templates/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ export { asText } from './text'
3030
export { asJson } from './json'
3131
export { asBase64 } from './base64'
3232
export { asBuffer } from './buffer'
33+
export { asCss } from './css'
3334

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { writeFileSync } from 'fs'
3636
/** Writes usage information */
3737
async function info() {
3838

39-
console.log(`Version 1.0.6
39+
console.log(`Version 1.0.7
4040
4141
Examples: tsc-bundle index.ts
4242
tsc-bundle tsconfig.json

0 commit comments

Comments
 (0)