Skip to content

Commit 578846b

Browse files
authored
Merge pull request #32 from andrewbranch/extension-dir
Add option to customize download location of extensions
2 parents 51714b4 + d92ed41 commit 578846b

File tree

6 files changed

+172
-103
lines changed

6 files changed

+172
-103
lines changed

README.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Includes OS dark mode support 🌙
1515
- [Languages](#languages)
1616
- [Themes](#themes)
1717
- [Using languages and themes from an extension](#using-languages-and-themes-from-an-extension)
18+
- [Dealing with rate limiting in CI](#dealing-with-rate-limiting-in-ci)
1819
- [Styles](#styles)
1920
- [Class names](#class-names)
2021
- [Variables](#variables)
@@ -56,17 +57,19 @@ Add to your `gatsby-config.js` (all options are optional; defaults shown here):
5657
// All options are optional. Defaults shown here.
5758
options: {
5859
colorTheme: 'Dark+ (default dark)', // Read on for list of included themes. Also accepts object and function forms.
59-
wrapperClassName: '', // Additional class put on 'pre' tag
60-
injectStyles: true, // Injects (minimal) additional CSS for layout and scrolling
61-
extensions: [], // Extensions to download from the marketplace to provide more languages and themes
62-
languageAliases: {}, // Map of custom/unknown language codes to standard/known language codes
63-
replaceColor: x => x, // Function allowing replacement of a theme color with another. Useful for replacing hex colors with CSS variables.
64-
getLineClassName: ({ // Function allowing dynamic setting of additional class names on individual lines
65-
content, // - the string content of the line
66-
index, // - the zero-based index of the line within the code fence
67-
language, // - the language specified for the code fence
68-
codeFenceOptions // - any options set on the code fence alongside the language (more on this later)
69-
}) => ''
60+
wrapperClassName: '', // Additional class put on 'pre' tag
61+
injectStyles: true, // Injects (minimal) additional CSS for layout and scrolling
62+
extensions: [], // Extensions to download from the marketplace to provide more languages and themes
63+
languageAliases: {}, // Map of custom/unknown language codes to standard/known language codes
64+
replaceColor: x => x, // Function allowing replacement of a theme color with another. Useful for replacing hex colors with CSS variables.
65+
getLineClassName: ({ // Function allowing dynamic setting of additional class names on individual lines
66+
content, // - the string content of the line
67+
index, // - the zero-based index of the line within the code fence
68+
language, // - the language specified for the code fence
69+
codeFenceOptions // - any options set on the code fence alongside the language (more on this later)
70+
}) => '',
71+
extensionDataDirectory: // Absolute path to the directory where extensions will be downloaded. Defaults to inside node_modules.
72+
path.resolve('extensions'),
7073
}
7174
}]
7275
}
@@ -211,7 +214,13 @@ Add those strings to the `extensions` option in your plugin configuration in `ga
211214
}]
212215
```
213216
214-
Next time you `gatsby develop` or `gatsby build`, the extension will be downloaded and Scala code fences will be highlighted. Extensions are downloaded to `node_modules/gatsby-remark-vscode/lib/extensions`, so they remain cached on disk as long as `gastsby-remark-vscode` does.
217+
Next time you `gatsby develop` or `gatsby build`, the extension will be downloaded and Scala code fences will be highlighted. Extensions are downloaded to `node_modules/gatsby-remark-vscode/lib/extensions` by default (but can go elsewhere by setting the `extensionDataDirectory` option), so they remain cached on disk as long as `gastsby-remark-vscode` does.
218+
219+
### Dealing with rate limiting in CI
220+
221+
Anonymous requests to the Visual Studio Marketplace are rate limited, so if you’re downloading a lot of extensions or running builds in quick succession in an environment where the extensions aren’t already cached on disk (like on a build server), you might see failed requests.
222+
223+
As a workaround, you can set the `extensionDataDirectory` plugin option to an absolute path pointing to a folder that you check into source control. After running a build locally, any extensions you’ve requested will appear in that directory. Then, in CI, gatsby-remark-vscode will check that directory and determine if anything needs to be downloaded. By checking including the extensions alongside your own source code, you can avoid making requests to the Visual Studio Marketplace in CI entirely.
215224
216225
## Styles
217226

scripts/scrapeBuiltinExtensions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const grammarPath = /** @param {string} basename */ basename => path.join(gramma
1717
const themePath = /** @param {string} basename */ basename => path.join(themeDestDir, basename);
1818
let languageId = 0;
1919

20-
glob(path.resolve(__dirname, '../vscode/extensions/**/package.json'), async (err, packages) => {
20+
glob(path.resolve(__dirname, '../vscode/extensions/*/package.json'), async (err, packages) => {
2121
try {
2222
if (err) throw err;
2323
await tryMkdir(grammarDestDir);

src/downloadExtension.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ async function mergeCache(cache, key, value) {
3535
/**
3636
* @param {import('.').ExtensionDemand} extensionDemand
3737
* @param {*} cache
38+
* @param {string} extensionDir
3839
*/
39-
async function syncExtensionData({ identifier }, cache) {
40-
const packageJsonPath = path.join(getExtensionPath(identifier), 'package.json');
40+
async function syncExtensionData({ identifier }, cache, extensionDir) {
41+
const packageJsonPath = path.join(getExtensionPath(identifier, extensionDir), 'package.json');
4142
const { grammars, themes } = await processExtension(packageJsonPath);
4243
Object.keys(grammars).forEach(scopeName => (grammars[scopeName].languageId = languageId++));
4344
await mergeCache(cache, 'grammars', grammars);
@@ -47,8 +48,9 @@ async function syncExtensionData({ identifier }, cache) {
4748
/**
4849
* @param {import('.').ExtensionDemand} extensionDemand
4950
* @param {*} cache
51+
* @param {string} extensionDir
5052
*/
51-
async function downloadExtension(extensionDemand, cache) {
53+
async function downloadExtension(extensionDemand, cache, extensionDir) {
5254
const { identifier, version } = extensionDemand;
5355
const { publisher, name } = parseExtensionIdentifier(identifier);
5456
const url = `https://marketplace.visualstudio.com/_apis/public/gallery/publishers/${publisher}/vsextensions/${name}/${version}/vspackage`;
@@ -73,37 +75,43 @@ async function downloadExtension(extensionDemand, cache) {
7375
});
7476
});
7577

76-
const extensionPath = getExtensionBasePath(identifier);
78+
const extensionPath = getExtensionBasePath(identifier, extensionDir);
7779
await decompress(archive, extensionPath);
78-
await syncExtensionData(extensionDemand, cache);
80+
await syncExtensionData(extensionDemand, cache, extensionDir);
7981
return extensionPath;
8082
}
8183

8284
/**
83-
* @param {'grammar' | 'theme'} type
84-
* @param {string} name
85-
* @param {import('.').ExtensionDemand[]} extensions
86-
* @param {*} cache
87-
* @param {Record<string, string>} languageAliases
85+
* @typedef {object} DownloadExtensionOptions
86+
* @property {'grammar' | 'theme'} type
87+
* @property {string} name
88+
* @property {import('.').ExtensionDemand[]} extensions
89+
* @property {*} cache
90+
* @property {Record<string, string>} languageAliases
91+
* @property {string} extensionDir
92+
*/
93+
94+
/**
95+
* @param {DownloadExtensionOptions} options
8896
*/
89-
async function downloadExtensionIfNeeded(type, name, extensions, cache, languageAliases) {
97+
async function downloadExtensionIfNeeded({ type, name, extensions, cache, languageAliases, extensionDir }) {
9098
extensions = extensions.slice();
9199
const extensionExists = type === 'grammar' ? grammarExists : themeExists;
92100
while (extensions.length && !(await extensionExists(name))) {
93101
const extensionDemand = extensions.shift();
94102
const { identifier, version } = extensionDemand;
95-
const extensionPath = getExtensionBasePath(identifier);
103+
const extensionPath = getExtensionBasePath(identifier, extensionDir);
96104
if (!fs.existsSync(extensionPath)) {
97-
await downloadExtension(extensionDemand, cache);
105+
await downloadExtension(extensionDemand, cache, extensionDir);
98106
continue;
99107
}
100-
const packageJson = getExtensionPackageJson(identifier);
108+
const packageJson = getExtensionPackageJson(identifier, extensionDir);
101109
if (packageJson.version !== version) {
102-
await downloadExtension(extensionDemand, cache);
110+
await downloadExtension(extensionDemand, cache, extensionDir);
103111
continue;
104112
}
105113

106-
await syncExtensionData(extensionDemand, cache);
114+
await syncExtensionData(extensionDemand, cache, extensionDir);
107115
}
108116

109117
/** @param {string} languageName */

src/index.js

Lines changed: 82 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ function getStylesFromSettings(settings) {
119119
* @property {(line: LineData) => string=} getLineClassName
120120
* @property {boolean=} injectStyles
121121
* @property {(colorValue: string, theme: string) => string=} replaceColor
122+
* @property {string=} extensionDataDirectory
122123
*/
123124

124125
function createPlugin() {
@@ -138,7 +139,8 @@ function createPlugin() {
138139
extensions = [],
139140
getLineClassName = () => '',
140141
injectStyles = true,
141-
replaceColor = x => x
142+
replaceColor = x => x,
143+
extensionDataDirectory = path.resolve(__dirname, '../lib/extensions')
142144
} = {}
143145
) {
144146
/** @type {Record<string, string>} */
@@ -153,7 +155,14 @@ function createPlugin() {
153155
const text = node.value || (node.children && node.children[0] && node.children[0].value);
154156
if (!text) continue;
155157
const { languageName, options } = parseCodeFenceHeader(node.lang ? node.lang.toLowerCase() : '');
156-
await downloadExtensionIfNeeded('grammar', languageName, extensions, cache, languageAliases);
158+
await downloadExtensionIfNeeded({
159+
type: 'grammar',
160+
name: languageName,
161+
extensions,
162+
cache,
163+
languageAliases,
164+
extensionDir: extensionDataDirectory
165+
});
157166

158167
const grammarCache = await cache.get('grammars');
159168
const scope = getScope(languageName, grammarCache, languageAliases);
@@ -166,68 +175,75 @@ function createPlugin() {
166175
warnMissingLanguageFile(missingScopeName, scope);
167176
});
168177

169-
const colorThemeValue =
170-
typeof colorTheme === 'function'
171-
? colorTheme({
172-
markdownNode,
173-
codeFenceNode: node,
174-
parsedOptions: options,
175-
language: languageName
176-
})
177-
: colorTheme;
178-
const colorThemeSettings = createColorThemeSettings(colorThemeValue);
179-
const themeClassNames = createThemeClassNames(colorThemeSettings);
180-
for (const setting in colorThemeSettings) {
181-
const colorThemeIdentifier = colorThemeSettings[setting];
182-
if (!colorThemeIdentifier) continue;
183-
await downloadExtensionIfNeeded('theme', colorThemeIdentifier, extensions, cache, languageAliases);
178+
try {
179+
const colorThemeValue =
180+
typeof colorTheme === 'function'
181+
? colorTheme({
182+
markdownNode,
183+
codeFenceNode: node,
184+
parsedOptions: options,
185+
language: languageName
186+
})
187+
: colorTheme;
188+
const colorThemeSettings = createColorThemeSettings(colorThemeValue);
189+
const themeClassNames = createThemeClassNames(colorThemeSettings);
190+
for (const setting in colorThemeSettings) {
191+
const colorThemeIdentifier = colorThemeSettings[setting];
192+
if (!colorThemeIdentifier) continue;
193+
await downloadExtensionIfNeeded({
194+
type: 'theme',
195+
name: colorThemeIdentifier,
196+
extensions,
197+
cache,
198+
languageAliases,
199+
extensionDir: extensionDataDirectory
200+
});
184201

185-
const themeClassName = themeClassNames[setting];
186-
const themeCache = await cache.get('themes');
187-
const colorThemePath =
188-
getThemeLocation(colorThemeIdentifier, themeCache) ||
189-
path.resolve(markdownNode.fileAbsolutePath, colorThemeIdentifier);
202+
const themeClassName = themeClassNames[setting];
203+
const themeCache = await cache.get('themes');
204+
const colorThemePath =
205+
getThemeLocation(colorThemeIdentifier, themeCache) ||
206+
path.resolve(markdownNode.fileAbsolutePath, colorThemeIdentifier);
190207

191-
const { resultRules: tokenColors, resultColors: settings } = loadColorTheme(colorThemePath);
192-
const defaultTokenColors = {
193-
settings: {
194-
foreground: settings['editor.foreground'] || settings.foreground,
195-
background: settings['editor.background'] || settings.background
196-
}
197-
};
208+
const { resultRules: tokenColors, resultColors: settings } = loadColorTheme(colorThemePath);
209+
const defaultTokenColors = {
210+
settings: {
211+
foreground: settings['editor.foreground'] || settings.foreground,
212+
background: settings['editor.background'] || settings.background
213+
}
214+
};
198215

199-
registry.setTheme({ settings: [defaultTokenColors, ...tokenColors] });
200-
if (!stylesheets[themeClassName]) {
201-
const rules = [
202-
renderRule(themeClassName, getStylesFromSettings(settings)),
203-
...(scope
204-
? prefixRules(
205-
generateTokensCSSForColorMap(
206-
registry.getColorMap().map(color => replaceColor(color, colorThemeIdentifier))
207-
).split('\n'),
208-
`.${themeClassName} `
209-
)
210-
: [])
211-
];
216+
registry.setTheme({ settings: [defaultTokenColors, ...tokenColors] });
217+
if (!stylesheets[themeClassName]) {
218+
const rules = [
219+
renderRule(themeClassName, getStylesFromSettings(settings)),
220+
...(scope
221+
? prefixRules(
222+
generateTokensCSSForColorMap(
223+
registry.getColorMap().map(color => replaceColor(color, colorThemeIdentifier))
224+
).split('\n'),
225+
`.${themeClassName} `
226+
)
227+
: [])
228+
];
212229

213-
if (setting === 'prefersDarkTheme') {
214-
stylesheets[themeClassName] = prefersDark(rules);
215-
} else if (setting === 'prefersLightTheme') {
216-
stylesheets[themeClassName] = prefersLight(rules);
217-
} else {
218-
stylesheets[themeClassName] = rules.join('\n');
230+
if (setting === 'prefersDarkTheme') {
231+
stylesheets[themeClassName] = prefersDark(rules);
232+
} else if (setting === 'prefersLightTheme') {
233+
stylesheets[themeClassName] = prefersLight(rules);
234+
} else {
235+
stylesheets[themeClassName] = rules.join('\n');
236+
}
219237
}
220238
}
221-
}
222239

223-
const rawLines = text.split(/\r?\n/);
224-
const htmlLines = [];
225-
/** @type {import('vscode-textmate').ITokenTypeMap} */
226-
let tokenTypes = {};
227-
/** @type {number} */
228-
let languageId;
240+
const rawLines = text.split(/\r?\n/);
241+
const htmlLines = [];
242+
/** @type {import('vscode-textmate').ITokenTypeMap} */
243+
let tokenTypes = {};
244+
/** @type {number} */
245+
let languageId;
229246

230-
try {
231247
if (scope) {
232248
const grammarData = getGrammar(scope, grammarCache);
233249
languageId = grammarData.languageId;
@@ -269,19 +285,19 @@ function createPlugin() {
269285

270286
htmlLines.push([`<span class="${className}">`, htmlLine, `</span>`].join(''));
271287
}
288+
289+
const className = joinClassNames(wrapperClassName, joinThemeClassNames(themeClassNames), 'vscode-highlight');
290+
node.type = 'html';
291+
node.value = [
292+
`<pre class="${className}" data-language="${languageName}">`,
293+
`<code class="vscode-highlight-code">`,
294+
htmlLines.join('\n'),
295+
`</code>`,
296+
`</pre>`
297+
].join('');
272298
} finally {
273299
unlockRegistry();
274300
}
275-
276-
const className = joinClassNames(wrapperClassName, joinThemeClassNames(themeClassNames), 'vscode-highlight');
277-
node.type = 'html';
278-
node.value = [
279-
`<pre class="${className}" data-language="${languageName}">`,
280-
`<code class="vscode-highlight-code">`,
281-
htmlLines.join('\n'),
282-
`</code>`,
283-
`</pre>`
284-
].join('');
285301
}
286302

287303
const themeNames = Object.keys(stylesheets);

src/utils.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,29 @@ function parseExtensionIdentifier(identifier) {
2222
/**
2323
* Gets the absolute path to the download path of a downloaded extension.
2424
* @param {string} identifier
25+
* @param {string} extensionDir
2526
*/
26-
function getExtensionBasePath(identifier) {
27-
return path.resolve(__dirname, '../lib/extensions', identifier);
27+
function getExtensionBasePath(identifier, extensionDir) {
28+
return path.join(extensionDir, identifier);
2829
}
2930

3031
/**
3132
* Gets the absolute path to the data directory of a downloaded extension.
3233
* @param {string} identifier
34+
* @param {string} extensionDir
3335
*/
34-
function getExtensionPath(identifier) {
35-
return path.resolve(getExtensionBasePath(identifier), 'extension');
36+
function getExtensionPath(identifier, extensionDir) {
37+
return path.join(getExtensionBasePath(identifier, extensionDir), 'extension');
3638
}
3739

3840
/**
3941
* Gets the package.json of an extension as a JavaScript object.
4042
* @param {string} identifier
43+
* @param {string} extensionDir
4144
* @returns {object}
4245
*/
43-
function getExtensionPackageJson(identifier) {
44-
return require(path.join(getExtensionPath(identifier), 'package.json'));
46+
function getExtensionPackageJson(identifier, extensionDir) {
47+
return require(path.join(getExtensionPath(identifier, extensionDir), 'package.json'));
4548
}
4649

4750
/**

0 commit comments

Comments
 (0)