@financial-times/dotcom-server-asset-loader 7.3.1 → 7.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@financial-times/dotcom-server-asset-loader",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/node/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"npm": "7.x || 8.x"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist/"
|
|
25
|
+
"dist/",
|
|
26
|
+
"src/"
|
|
26
27
|
],
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
|
@@ -39,4 +40,4 @@
|
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"check-engine": "^1.10.1"
|
|
41
42
|
}
|
|
42
|
-
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import urlJoin from 'url-join'
|
|
3
|
+
import { loadFile } from './helpers/loadFile'
|
|
4
|
+
import { loadManifest } from './helpers/loadManifest'
|
|
5
|
+
|
|
6
|
+
export interface AssetLoaderOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The name of the asset manifest file
|
|
9
|
+
* @default "manifest.json"
|
|
10
|
+
*/
|
|
11
|
+
manifestFileName?: string
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The public-facing URL for the static assets
|
|
15
|
+
*/
|
|
16
|
+
publicPath?: string
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The absolute path to the directory of static assets
|
|
20
|
+
* @default path.resolve('./public')
|
|
21
|
+
*/
|
|
22
|
+
fileSystemPath?: string
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Store files in memory when accessed
|
|
26
|
+
* @default false
|
|
27
|
+
*/
|
|
28
|
+
cacheFileContents?: boolean
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The asset manifest
|
|
32
|
+
*/
|
|
33
|
+
manifest?: { [asset: string]: string }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultOptions: AssetLoaderOptions = {
|
|
37
|
+
publicPath: '/',
|
|
38
|
+
manifestFileName: 'manifest.json',
|
|
39
|
+
fileSystemPath: path.resolve('./public'),
|
|
40
|
+
cacheFileContents: false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type TEntrypoint = {
|
|
44
|
+
[type: string]: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type TEntrypoints = {
|
|
48
|
+
entrypoints?: {
|
|
49
|
+
[name: string]: TEntrypoint
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type TFiles = {
|
|
54
|
+
[name: string]: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type TManifest = TEntrypoints & TFiles
|
|
58
|
+
|
|
59
|
+
export class AssetLoader {
|
|
60
|
+
public options: AssetLoaderOptions
|
|
61
|
+
public manifest: TManifest
|
|
62
|
+
|
|
63
|
+
constructor(userOptions?: AssetLoaderOptions) {
|
|
64
|
+
this.options = { ...defaultOptions, ...userOptions }
|
|
65
|
+
this.manifest =
|
|
66
|
+
this.options.manifest ||
|
|
67
|
+
loadManifest(path.resolve(this.options.fileSystemPath, this.options.manifestFileName))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getHashedAsset(asset: string): string {
|
|
71
|
+
if (this.manifest.hasOwnProperty(asset)) {
|
|
72
|
+
return this.manifest[asset]
|
|
73
|
+
} else {
|
|
74
|
+
throw Error(`Couldn't find asset "${asset}" in manifest`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getFileContents(asset: string): string {
|
|
79
|
+
return loadFile(this.getFileSystemPath(asset), this.options.cacheFileContents)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getFileSystemPath(asset: string): string {
|
|
83
|
+
return this.formatFileSystemPath(this.getHashedAsset(asset))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getPublicURL(asset: string): string {
|
|
87
|
+
return this.formatPublicURL(this.getHashedAsset(asset))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//
|
|
91
|
+
// File name prefix methods
|
|
92
|
+
//
|
|
93
|
+
|
|
94
|
+
formatPublicURL(hashedAsset: string): string {
|
|
95
|
+
return urlJoin(this.options.publicPath, hashedAsset)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
formatFileSystemPath(hashedAsset: string): string {
|
|
99
|
+
return path.join(this.options.fileSystemPath, hashedAsset)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//
|
|
103
|
+
// Webpack entry point methods
|
|
104
|
+
//
|
|
105
|
+
|
|
106
|
+
getFilesFor(entrypoint: string): TEntrypoint {
|
|
107
|
+
if (this.manifest.entrypoints && this.manifest.entrypoints[entrypoint]) {
|
|
108
|
+
return this.manifest.entrypoints[entrypoint]
|
|
109
|
+
} else {
|
|
110
|
+
throw Error(`Couldn't find entrypoint "${entrypoint}" in manifest`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getScriptFilesFor(entrypoint: string): string[] {
|
|
115
|
+
return this.getFilesFor(entrypoint).js || []
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getStylesheetFilesFor(entrypoint: string): string[] {
|
|
119
|
+
return this.getFilesFor(entrypoint).css || []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getScriptPathsFor(entrypoint: string): string[] {
|
|
123
|
+
return this.getScriptFilesFor(entrypoint).map(this.formatFileSystemPath.bind(this))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getStylesheetPathsFor(entrypoint: string): string[] {
|
|
127
|
+
return this.getStylesheetFilesFor(entrypoint).map(this.formatFileSystemPath.bind(this))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getScriptURLsFor(entrypoint: string): string[] {
|
|
131
|
+
return this.getScriptFilesFor(entrypoint).map(this.formatPublicURL.bind(this))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getStylesheetURLsFor(entrypoint: string): string[] {
|
|
135
|
+
return this.getStylesheetFilesFor(entrypoint).map(this.formatPublicURL.bind(this))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { AssetLoader, AssetLoaderOptions } from '../AssetLoader'
|
|
2
|
+
import manifest from './__fixtures__/manifest.json'
|
|
3
|
+
|
|
4
|
+
jest.mock('../helpers/loadManifest', () => {
|
|
5
|
+
return {
|
|
6
|
+
loadManifest: jest.fn(() => manifest)
|
|
7
|
+
}
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
jest.mock('../helpers/loadFile', () => {
|
|
11
|
+
return {
|
|
12
|
+
loadFile: jest.fn(() => {
|
|
13
|
+
return 'FILE CONTENTS'
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
function createAssetLoader(options?: AssetLoaderOptions) {
|
|
19
|
+
return new AssetLoader(options)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('dotcom-server-asset-loader/src/AssetLoader', () => {
|
|
23
|
+
let loader
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
loader = createAssetLoader({
|
|
27
|
+
publicPath: '/public/assets/',
|
|
28
|
+
fileSystemPath: '/internal/path/to/assets'
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
jest.restoreAllMocks()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('constructor', () => {
|
|
37
|
+
it('uses the supplied manifest instead of looking it up', () => {
|
|
38
|
+
const manifest = { foo: 'bar' }
|
|
39
|
+
const loader = createAssetLoader({ manifest })
|
|
40
|
+
const result = loader.getHashedAsset('foo')
|
|
41
|
+
expect(result).toBe(manifest.foo)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('.getHashedAsset()', () => {
|
|
46
|
+
it('returns the hashed name from a manifest', () => {
|
|
47
|
+
const result = loader.getHashedAsset('styles.css')
|
|
48
|
+
expect(result).toEqual('styles.12345.bundle.css')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("errors if the file can't be found in the manifest", () => {
|
|
52
|
+
expect(() => {
|
|
53
|
+
loader.getHashedAsset('test')
|
|
54
|
+
}).toThrow(Error('Couldn\'t find asset "test" in manifest'))
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('.getFileSystemPath()', () => {
|
|
59
|
+
it('returns the file system path for the requested file', () => {
|
|
60
|
+
const result = loader.getFileSystemPath('styles.css')
|
|
61
|
+
expect(result).toEqual('/internal/path/to/assets/styles.12345.bundle.css')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('.getPublicURL()', () => {
|
|
66
|
+
const tests = [
|
|
67
|
+
{ expected: '/styles.12345.bundle.css' },
|
|
68
|
+
{ publicPath: '', expected: 'styles.12345.bundle.css' },
|
|
69
|
+
{ publicPath: '/', expected: '/styles.12345.bundle.css' },
|
|
70
|
+
{ publicPath: '/lib', expected: '/lib/styles.12345.bundle.css' },
|
|
71
|
+
{ publicPath: '/lib/', expected: '/lib/styles.12345.bundle.css' },
|
|
72
|
+
{ publicPath: '../', expected: '../styles.12345.bundle.css' }
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
tests.forEach(({ publicPath, expected }) => {
|
|
76
|
+
it(`publicPath: ${publicPath}`, () => {
|
|
77
|
+
const loader = typeof publicPath === 'undefined' ? new AssetLoader() : new AssetLoader({ publicPath })
|
|
78
|
+
const result = loader.getPublicURL('styles.css')
|
|
79
|
+
expect(result).toEqual(expected)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('.getFileContents()', () => {
|
|
85
|
+
it('returns the file contents for the requested file', () => {
|
|
86
|
+
const result = loader.getFileContents('styles.css')
|
|
87
|
+
expect(result).toEqual('FILE CONTENTS')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('.getFilesFor()', () => {
|
|
92
|
+
it('returns all file types', () => {
|
|
93
|
+
const result = loader.getFilesFor('main')
|
|
94
|
+
|
|
95
|
+
expect(Object.keys(result)).toEqual(['js', 'css'])
|
|
96
|
+
expect(result.js).toEqual(expect.any(Array))
|
|
97
|
+
expect(result.css).toEqual(expect.any(Array))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('throws if the entry point cannot be found', () => {
|
|
101
|
+
expect(() => loader.getFilesFor('third')).toThrow()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('.getScriptFilesFor()', () => {
|
|
106
|
+
it('returns an array', () => {
|
|
107
|
+
const result = loader.getScriptFilesFor('main')
|
|
108
|
+
expect(result).toEqual(expect.any(Array))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('returns an array of JS files', () => {
|
|
112
|
+
const result = loader.getScriptFilesFor('main')
|
|
113
|
+
|
|
114
|
+
expect(result.length).toBe(3)
|
|
115
|
+
|
|
116
|
+
result.forEach((item) => {
|
|
117
|
+
expect(item).toMatch(/\.js$/)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('.getStylesheetFilesFor()', () => {
|
|
123
|
+
it('returns an array of CSS files', () => {
|
|
124
|
+
const result = loader.getStylesheetFilesFor('main')
|
|
125
|
+
|
|
126
|
+
expect(result.length).toBe(2)
|
|
127
|
+
|
|
128
|
+
result.forEach((item) => {
|
|
129
|
+
expect(item).toMatch(/\.css$/)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('.getScriptPathsFor()', () => {
|
|
135
|
+
it('returns an array of JS file paths', () => {
|
|
136
|
+
const result = loader.getScriptPathsFor('main')
|
|
137
|
+
|
|
138
|
+
expect(result.length).toBe(3)
|
|
139
|
+
|
|
140
|
+
result.forEach((item) => {
|
|
141
|
+
expect(item).toMatch(/^\/internal\/path\/to\/assets\/.+\.js$/)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('.getStylesheetPathsFor()', () => {
|
|
147
|
+
it('returns an array of CSS file paths', () => {
|
|
148
|
+
const result = loader.getStylesheetPathsFor('main')
|
|
149
|
+
|
|
150
|
+
expect(result.length).toBe(2)
|
|
151
|
+
|
|
152
|
+
result.forEach((item) => {
|
|
153
|
+
expect(item).toMatch(/^\/internal\/path\/to\/assets\/.+\.css$/)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('.getScriptURLsFor()', () => {
|
|
159
|
+
it('returns an array of JS file URLs', () => {
|
|
160
|
+
const result = loader.getScriptURLsFor('main')
|
|
161
|
+
|
|
162
|
+
expect(result.length).toBe(3)
|
|
163
|
+
|
|
164
|
+
result.forEach((item) => {
|
|
165
|
+
expect(item).toMatch(/^\/public\/assets\/.+\.js$/)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('.getStylesheetURLsFor()', () => {
|
|
171
|
+
it('returns an array of CSS file URLs', () => {
|
|
172
|
+
const result = loader.getStylesheetURLsFor('main')
|
|
173
|
+
|
|
174
|
+
expect(result.length).toBe(2)
|
|
175
|
+
|
|
176
|
+
result.forEach((item) => {
|
|
177
|
+
expect(item).toMatch(/^\/public\/assets\/.+\.css$/)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"styles.css": "styles.12345.bundle.css",
|
|
3
|
+
"main.js": "main.12345.bundle.js",
|
|
4
|
+
"secondary.js": "secondary.12345.bundle.js",
|
|
5
|
+
"vendor.foo.js": "vendor.foo.12345.bundle.js",
|
|
6
|
+
"vendor.bar.js": "vendor.bar.12345.bundle.js",
|
|
7
|
+
"vendor.baz.js": "vendor.baz.12345.bundle.js",
|
|
8
|
+
"runtime.js": "runtime.bundle.js",
|
|
9
|
+
"entrypoints": {
|
|
10
|
+
"main": {
|
|
11
|
+
"js": ["vendor.foo.12345.bundle.js", "vendor.bar.12345.bundle.js", "main.12345.bundle.js"],
|
|
12
|
+
"css": ["styles.12345.bundle.css", "lazyload.12345.bundle.css"]
|
|
13
|
+
},
|
|
14
|
+
"secondary": {
|
|
15
|
+
"js": ["vendor.foo.12345.bundle.js", "vendor.baz.12345.bundle.js", "secondary.12345.bundle.js"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
|
|
3
|
+
// Avoid hitting the disk each time a file is requested and instead
|
|
4
|
+
// hold the file contents in memory for the lifecycle of the app
|
|
5
|
+
const store = new Map()
|
|
6
|
+
|
|
7
|
+
export function loadFile(fullPath: string, cache: boolean = false): string {
|
|
8
|
+
if (store.has(fullPath)) {
|
|
9
|
+
return store.get(fullPath)
|
|
10
|
+
} else {
|
|
11
|
+
const fileAsBuffer = fs.readFileSync(fullPath)
|
|
12
|
+
const fileAsString = String(fileAsBuffer)
|
|
13
|
+
|
|
14
|
+
if (cache) {
|
|
15
|
+
store.set(fullPath, fileAsString)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return fileAsString
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AssetLoader'
|