vite-plugin-keywords 1.1.1 → 1.2.0

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/README.md CHANGED
@@ -177,12 +177,42 @@ pnpm add -D vite-plugin-keywords
177
177
 
178
178
  5. The `.keywords/index.d.ts` type file is created automatically on `vite dev/build`, or manually via the `keywords` script.
179
179
 
180
+ ## CLI
181
+
182
+ You can manually generate the keyword types using the provided CLI. This is useful for type-checking in CI or before a build.
183
+
184
+ ```bash
185
+ npx keywords [--config <path>]
186
+ ```
187
+
188
+ | Option | Shorthand | Description |
189
+ | ---------- | --------- | ---------------------------------------------- |
190
+ | `--config` | `-c` | Specify a custom Vite configuration file path. |
191
+
180
192
  ## Options
181
193
 
182
- None
194
+ ### `additionalModulesToScan`
195
+
196
+ - **Type:** `string[]`
197
+ - **Default:** `[]`
198
+
199
+ An array of module specifiers (e.g. `'react'` or `'@vue/runtime-core'`) that the plugin should scan for keyword usage in addition to `virtual:keywords`. This is useful if you alias internal module exports that you wish to have extracted as keywords automatically.
200
+
201
+ ```ts
202
+ import keywords from 'vite-plugin-keywords';
203
+
204
+ export default defineConfig({
205
+ plugins: [
206
+ keywords({
207
+ additionalModulesToScan: ['react'],
208
+ }),
209
+ ],
210
+ });
211
+ ```
183
212
 
184
213
  ## Limitations
185
214
 
215
+ - **Serialization**: Keywords are compiled down to `Symbol` primitives, which cannot be serialized (e.g., via `JSON.stringify()`). Avoid using keywords for keys in state objects that need to be persisted to LocalStorage, sent over network boundaries, or used in SSR hydration.
186
216
  - **(TODO) Frameworks**: The plugin uses Babel to parse JavaScript and TypeScript files. It cannot parse keywords from Vue, Svelte, or Astro files yet.
187
217
  - **Dynamic Access**: Only static property access (e.g., `K.myKeyword`) is detected. Dynamic, computed access (e.g., `K['myKeyword']`) will not be identified by the plugin.
188
218
 
package/dist/cli.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/cli.ts
2
+ import { parseArgs } from "util";
3
+
1
4
  // ../minifiable-keywords/dist/index.js
2
5
  import { mkdir, readFile, writeFile } from "fs/promises";
3
6
  import path from "path";
@@ -6,6 +9,11 @@ import _traverse from "@babel/traverse";
6
9
  import { globby } from "globby";
7
10
  var VIRTUAL_MODULE_ID = "virtual:keywords";
8
11
  var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
12
+ var buildOptions = (options = {}) => {
13
+ return {
14
+ additionalModulesToScan: options.additionalModulesToScan || []
15
+ };
16
+ };
9
17
  var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
10
18
  const prefix = usePrefix ? `[${pluginName}] ` : "";
11
19
  const prefixed = (message) => `${prefix}${message}`;
@@ -17,8 +25,12 @@ var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
17
25
  };
18
26
  };
19
27
  var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
20
- var extractKeywords = (code) => {
28
+ var extractKeywords = (code, additionalModulesToScan = []) => {
21
29
  const keywords = /* @__PURE__ */ new Set();
30
+ const containsTargetModule = code.includes(VIRTUAL_MODULE_ID) || additionalModulesToScan.some((moduleName) => code.includes(moduleName));
31
+ if (!containsTargetModule) {
32
+ return keywords;
33
+ }
22
34
  let ast;
23
35
  try {
24
36
  ast = parse(code, {
@@ -33,17 +45,20 @@ var extractKeywords = (code) => {
33
45
  traverse(ast, {
34
46
  enter(nodePath) {
35
47
  const node = nodePath.node;
36
- if (node.type === "ImportDeclaration" && node.source.value === VIRTUAL_MODULE_ID) {
37
- for (const specifier of node.specifiers) {
38
- if (specifier.type === "ImportNamespaceSpecifier") {
39
- keywordNamespaces.add(specifier.local.name);
40
- }
41
- if (specifier.type === "ImportDefaultSpecifier") {
42
- keywords.add("default");
43
- }
44
- if (specifier.type === "ImportSpecifier") {
45
- if (specifier.imported.type === "Identifier") {
46
- keywords.add(specifier.imported.name);
48
+ if (node.type === "ImportDeclaration") {
49
+ const isTargetModule = node.source.value === VIRTUAL_MODULE_ID || additionalModulesToScan.includes(node.source.value);
50
+ if (isTargetModule) {
51
+ for (const specifier of node.specifiers) {
52
+ if (specifier.type === "ImportNamespaceSpecifier") {
53
+ keywordNamespaces.add(specifier.local.name);
54
+ }
55
+ if (specifier.type === "ImportDefaultSpecifier") {
56
+ keywords.add("default");
57
+ }
58
+ if (specifier.type === "ImportSpecifier") {
59
+ if (specifier.imported.type === "Identifier") {
60
+ keywords.add(specifier.imported.name);
61
+ }
47
62
  }
48
63
  }
49
64
  }
@@ -85,7 +100,7 @@ ${exportDeclaration}
85
100
  await writeFile(path.join(pluginRoot, filename), `${content.trim()}
86
101
  `);
87
102
  };
88
- var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
103
+ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = [], options = buildOptions()) => {
89
104
  const collectedKeywords = /* @__PURE__ */ new Set();
90
105
  logger.info("Scanning project files for keywords...");
91
106
  const files = await globby("**/*.{js,ts,jsx,tsx}", {
@@ -94,25 +109,37 @@ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
94
109
  ignore: ["**/node_modules/**", ...ignoredDirs.map((dir) => `${dir}/**`)],
95
110
  gitignore: true
96
111
  });
97
- await Promise.all(
98
- files.map(async (file) => {
99
- const code = await readFile(file, "utf-8");
100
- const keywords = extractKeywords(code);
101
- for (const key of keywords) {
102
- collectedKeywords.add(key);
103
- }
104
- })
105
- );
112
+ const concurrency = 100;
113
+ for (let i = 0; i < files.length; i += concurrency) {
114
+ const chunk = files.slice(i, i + concurrency);
115
+ await Promise.all(
116
+ chunk.map(async (file) => {
117
+ try {
118
+ const code = await readFile(file, "utf-8");
119
+ const keywords = extractKeywords(
120
+ code,
121
+ options.additionalModulesToScan
122
+ );
123
+ for (const key of keywords) {
124
+ collectedKeywords.add(key);
125
+ }
126
+ } catch (error) {
127
+ logger.warn(`Failed to process file ${file}: ${error.message}`);
128
+ }
129
+ })
130
+ );
131
+ }
106
132
  logger.info(
107
133
  `Scan complete. Found ${collectedKeywords.size} unique keywords.`
108
134
  );
109
135
  return collectedKeywords;
110
136
  };
111
- var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs) => {
137
+ var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs, options) => {
112
138
  const collectedKeywords = await collectKeywordsFromFiles(
113
139
  root,
114
140
  logger,
115
- ignoredDirs
141
+ ignoredDirs,
142
+ options
116
143
  );
117
144
  await generateTypesFile(collectedKeywords, root);
118
145
  return collectedKeywords;
@@ -126,12 +153,53 @@ var PLUGIN_NAME = "vite-plugin-keywords";
126
153
 
127
154
  // src/cli.ts
128
155
  var main = async () => {
129
- const config = await resolveConfig({}, "build");
156
+ const { values } = parseArgs({
157
+ options: {
158
+ config: {
159
+ type: "string",
160
+ short: "c"
161
+ }
162
+ },
163
+ strict: false
164
+ });
165
+ let config;
166
+ try {
167
+ config = await resolveConfig(
168
+ {
169
+ configFile: typeof values.config === "string" ? values.config : void 0
170
+ },
171
+ "build"
172
+ );
173
+ } catch (error) {
174
+ console.error(
175
+ `[${PLUGIN_NAME}] Failed to resolve Vite config: ${error.message}`
176
+ );
177
+ process.exit(1);
178
+ }
130
179
  const logger = createPrefixedLogger(config.logger, PLUGIN_NAME);
131
- await collectKeywordsAndGenerateTypes(config.root, logger, [
132
- config.build.outDir,
133
- config.cacheDir
134
- ]);
180
+ const keywordsPlugin = config.plugins.find(
181
+ (plugin) => plugin.name === PLUGIN_NAME
182
+ );
183
+ let pluginOptions;
184
+ if (keywordsPlugin) {
185
+ pluginOptions = keywordsPlugin.api?.options;
186
+ } else {
187
+ logger.error("Keywords plugin not found in Vite configuration.");
188
+ process.exit(1);
189
+ }
190
+ try {
191
+ await collectKeywordsAndGenerateTypes(
192
+ config.root,
193
+ logger,
194
+ [config.build.outDir, config.cacheDir],
195
+ pluginOptions
196
+ );
197
+ } catch (error) {
198
+ logger.error(
199
+ `Failed to collect keywords and generate types: ${error.message}`
200
+ );
201
+ process.exit(1);
202
+ }
135
203
  };
136
204
  await main();
137
205
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../minifiable-keywords/src/index.ts","../src/cli.ts","../src/shared.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (code: string): Set<string> => {\n const keywords = new Set<string>();\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'ImportDeclaration' &&\n node.source.value === VIRTUAL_MODULE_ID\n ) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'index.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `declare const ${keywordConstPrefix}${key}: unique symbol;`)\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n const content = `${keywordDeclarations}\\n${exportDeclaration}\\n`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n await Promise.all(\n files.map(async (file) => {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(code);\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n }),\n );\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","import {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n} from 'minifiable-keywords';\nimport { resolveConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nconst main = async () => {\n const config = await resolveConfig({}, 'build');\n const logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n\n // const keywordsPlugin = config.plugins.find(\n // (plugin) => plugin.name === PLUGIN_NAME,\n // );\n // if (!keywordsPlugin) {\n // logger.error('Keywords plugin not found in Vite configuration.');\n // process.exit(1);\n // }\n\n await collectKeywordsAndGenerateTypes(config.root, logger, [\n config.build.outDir,\n config.cacheDir,\n ]);\n};\n\nawait main();\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts","../../minifiable-keywords/src/index.ts","../src/shared.ts"],"sourcesContent":["import { parseArgs } from 'node:util';\nimport {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n} from 'minifiable-keywords';\nimport { resolveConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nconst main = async () => {\n const { values } = parseArgs({\n options: {\n config: {\n type: 'string',\n short: 'c',\n },\n },\n strict: false,\n });\n\n let config;\n try {\n config = await resolveConfig(\n {\n configFile:\n typeof values.config === 'string' ? values.config : undefined,\n },\n 'build',\n );\n } catch (error: any) {\n console.error(\n `[${PLUGIN_NAME}] Failed to resolve Vite config: ${error.message}`,\n );\n process.exit(1);\n }\n\n const logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n\n const keywordsPlugin = config.plugins.find(\n (plugin) => plugin.name === PLUGIN_NAME,\n );\n\n let pluginOptions;\n if (keywordsPlugin) {\n pluginOptions = keywordsPlugin.api?.options;\n } else {\n logger.error('Keywords plugin not found in Vite configuration.');\n process.exit(1);\n }\n\n try {\n await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n pluginOptions,\n );\n } catch (error: any) {\n logger.error(\n `Failed to collect keywords and generate types: ${error.message}`,\n );\n process.exit(1);\n }\n};\n\nawait main();\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface KeywordsPluginOptions {\n additionalModulesToScan: string[];\n}\n\nexport const buildOptions = (\n options: Partial<KeywordsPluginOptions> = {},\n): KeywordsPluginOptions => {\n return {\n additionalModulesToScan: options.additionalModulesToScan || [],\n };\n};\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (\n code: string,\n additionalModulesToScan: string[] = [],\n): Set<string> => {\n const keywords = new Set<string>();\n\n // Fast-path: Skip parsing if no relevant imports are present in the code.\n const containsTargetModule =\n code.includes(VIRTUAL_MODULE_ID) ||\n additionalModulesToScan.some((moduleName) => code.includes(moduleName));\n\n if (!containsTargetModule) {\n return keywords;\n }\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (node.type === 'ImportDeclaration') {\n const isTargetModule =\n node.source.value === VIRTUAL_MODULE_ID ||\n additionalModulesToScan.includes(node.source.value);\n\n if (isTargetModule) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'index.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `declare const ${keywordConstPrefix}${key}: unique symbol;`)\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n const content = `${keywordDeclarations}\\n${exportDeclaration}\\n`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n options: KeywordsPluginOptions = buildOptions(),\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n const concurrency = 100;\n for (let i = 0; i < files.length; i += concurrency) {\n const chunk = files.slice(i, i + concurrency);\n await Promise.all(\n chunk.map(async (file) => {\n try {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(\n code,\n options.additionalModulesToScan,\n );\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n } catch (error: any) {\n logger.warn(`Failed to process file ${file}: ${error.message}`);\n }\n }),\n );\n }\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n options?: KeywordsPluginOptions,\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n options,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -44,6 +44,11 @@ var import_traverse = __toESM(require("@babel/traverse"), 1);
44
44
  var import_globby = require("globby");
45
45
  var VIRTUAL_MODULE_ID = "virtual:keywords";
46
46
  var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
47
+ var buildOptions = (options = {}) => {
48
+ return {
49
+ additionalModulesToScan: options.additionalModulesToScan || []
50
+ };
51
+ };
47
52
  var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
48
53
  const prefix = usePrefix ? `[${pluginName}] ` : "";
49
54
  const prefixed = (message) => `${prefix}${message}`;
@@ -55,8 +60,12 @@ var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
55
60
  };
56
61
  };
57
62
  var traverse = typeof import_traverse.default === "function" ? import_traverse.default : import_traverse.default.default;
58
- var extractKeywords = (code) => {
63
+ var extractKeywords = (code, additionalModulesToScan = []) => {
59
64
  const keywords = /* @__PURE__ */ new Set();
65
+ const containsTargetModule = code.includes(VIRTUAL_MODULE_ID) || additionalModulesToScan.some((moduleName) => code.includes(moduleName));
66
+ if (!containsTargetModule) {
67
+ return keywords;
68
+ }
60
69
  let ast;
61
70
  try {
62
71
  ast = (0, import_parser.parse)(code, {
@@ -71,17 +80,20 @@ var extractKeywords = (code) => {
71
80
  traverse(ast, {
72
81
  enter(nodePath) {
73
82
  const node = nodePath.node;
74
- if (node.type === "ImportDeclaration" && node.source.value === VIRTUAL_MODULE_ID) {
75
- for (const specifier of node.specifiers) {
76
- if (specifier.type === "ImportNamespaceSpecifier") {
77
- keywordNamespaces.add(specifier.local.name);
78
- }
79
- if (specifier.type === "ImportDefaultSpecifier") {
80
- keywords.add("default");
81
- }
82
- if (specifier.type === "ImportSpecifier") {
83
- if (specifier.imported.type === "Identifier") {
84
- keywords.add(specifier.imported.name);
83
+ if (node.type === "ImportDeclaration") {
84
+ const isTargetModule = node.source.value === VIRTUAL_MODULE_ID || additionalModulesToScan.includes(node.source.value);
85
+ if (isTargetModule) {
86
+ for (const specifier of node.specifiers) {
87
+ if (specifier.type === "ImportNamespaceSpecifier") {
88
+ keywordNamespaces.add(specifier.local.name);
89
+ }
90
+ if (specifier.type === "ImportDefaultSpecifier") {
91
+ keywords.add("default");
92
+ }
93
+ if (specifier.type === "ImportSpecifier") {
94
+ if (specifier.imported.type === "Identifier") {
95
+ keywords.add(specifier.imported.name);
96
+ }
85
97
  }
86
98
  }
87
99
  }
@@ -123,7 +135,7 @@ ${exportDeclaration}
123
135
  await (0, import_promises.writeFile)(import_path.default.join(pluginRoot, filename), `${content.trim()}
124
136
  `);
125
137
  };
126
- var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
138
+ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = [], options = buildOptions()) => {
127
139
  const collectedKeywords = /* @__PURE__ */ new Set();
128
140
  logger.info("Scanning project files for keywords...");
129
141
  const files = await (0, import_globby.globby)("**/*.{js,ts,jsx,tsx}", {
@@ -132,25 +144,37 @@ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
132
144
  ignore: ["**/node_modules/**", ...ignoredDirs.map((dir) => `${dir}/**`)],
133
145
  gitignore: true
134
146
  });
135
- await Promise.all(
136
- files.map(async (file) => {
137
- const code = await (0, import_promises.readFile)(file, "utf-8");
138
- const keywords = extractKeywords(code);
139
- for (const key of keywords) {
140
- collectedKeywords.add(key);
141
- }
142
- })
143
- );
147
+ const concurrency = 100;
148
+ for (let i = 0; i < files.length; i += concurrency) {
149
+ const chunk = files.slice(i, i + concurrency);
150
+ await Promise.all(
151
+ chunk.map(async (file) => {
152
+ try {
153
+ const code = await (0, import_promises.readFile)(file, "utf-8");
154
+ const keywords = extractKeywords(
155
+ code,
156
+ options.additionalModulesToScan
157
+ );
158
+ for (const key of keywords) {
159
+ collectedKeywords.add(key);
160
+ }
161
+ } catch (error) {
162
+ logger.warn(`Failed to process file ${file}: ${error.message}`);
163
+ }
164
+ })
165
+ );
166
+ }
144
167
  logger.info(
145
168
  `Scan complete. Found ${collectedKeywords.size} unique keywords.`
146
169
  );
147
170
  return collectedKeywords;
148
171
  };
149
- var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs) => {
172
+ var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs, options) => {
150
173
  const collectedKeywords = await collectKeywordsFromFiles(
151
174
  root,
152
175
  logger,
153
- ignoredDirs
176
+ ignoredDirs,
177
+ options
154
178
  );
155
179
  await generateTypesFile(collectedKeywords, root);
156
180
  return collectedKeywords;
@@ -173,7 +197,8 @@ var splitQuery = (id) => id.split("?");
173
197
  var PLUGIN_NAME = "vite-plugin-keywords";
174
198
 
175
199
  // src/index.ts
176
- var keywordsPlugin = () => {
200
+ var keywordsPlugin = (options) => {
201
+ const pluginOptions = buildOptions(options);
177
202
  let collectedKeywords;
178
203
  let config;
179
204
  let logger;
@@ -186,6 +211,9 @@ var keywordsPlugin = () => {
186
211
  };
187
212
  return {
188
213
  name: PLUGIN_NAME,
214
+ api: {
215
+ options: pluginOptions
216
+ },
189
217
  configResolved(resolvedConfig) {
190
218
  config = resolvedConfig;
191
219
  logger = createPrefixedLogger(config.logger, PLUGIN_NAME);
@@ -194,7 +222,8 @@ var keywordsPlugin = () => {
194
222
  collectedKeywords = await collectKeywordsAndGenerateTypes(
195
223
  config.root,
196
224
  logger,
197
- [config.build.outDir, config.cacheDir]
225
+ [config.build.outDir, config.cacheDir],
226
+ pluginOptions
198
227
  );
199
228
  },
200
229
  resolveId(source, importer) {
@@ -220,7 +249,10 @@ var keywordsPlugin = () => {
220
249
  return;
221
250
  }
222
251
  const code = await read();
223
- const keywordsInFile = extractKeywords(code);
252
+ const keywordsInFile = extractKeywords(
253
+ code,
254
+ pluginOptions.additionalModulesToScan
255
+ );
224
256
  if (keywordsInFile.size === 0) return;
225
257
  const initialSize = collectedKeywords.size;
226
258
  for (const key of keywordsInFile) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../minifiable-keywords/src/index.ts","../src/shared.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n extractKeywords,\n generateModuleCode,\n generateTypesFile,\n RESOLVED_VIRTUAL_MODULE_ID,\n splitQuery,\n VIRTUAL_MODULE_ID,\n type PrefixedLogger,\n} from 'minifiable-keywords';\nimport type { EnvironmentModuleGraph, Plugin, ResolvedConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nexport const keywordsPlugin = (): Plugin => {\n let collectedKeywords: Set<string>;\n let config: ResolvedConfig;\n let logger: PrefixedLogger;\n\n const invalidateModule = (\n absoluteId: string,\n moduleGraph: EnvironmentModuleGraph,\n ): void => {\n const module = moduleGraph.getModuleById(absoluteId);\n if (module) {\n moduleGraph.invalidateModule(module);\n module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now();\n }\n };\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n },\n\n async buildStart() {\n collectedKeywords = await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n );\n },\n\n resolveId(source, importer) {\n if (!importer) {\n return;\n }\n const [validSource] = splitQuery(source);\n if (validSource === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id) {\n const [validId] = splitQuery(id);\n if (validId === RESOLVED_VIRTUAL_MODULE_ID) {\n const isDev = config.mode === 'development';\n return generateModuleCode(collectedKeywords, isDev);\n }\n },\n\n async hotUpdate({ type, file, read }) {\n if (type === 'delete') return;\n\n const fileExt = path.extname(file);\n if (\n !['.js', '.ts', '.jsx', '.tsx'].includes(fileExt) ||\n file.includes('/.')\n ) {\n return;\n }\n\n const code = await read();\n const keywordsInFile = extractKeywords(code);\n if (keywordsInFile.size === 0) return;\n\n const initialSize = collectedKeywords.size;\n for (const key of keywordsInFile) {\n collectedKeywords.add(key);\n }\n\n const newKeywordsAdded = collectedKeywords.size > initialSize;\n if (newKeywordsAdded) {\n invalidateModule(\n RESOLVED_VIRTUAL_MODULE_ID,\n this.environment.moduleGraph,\n );\n await generateTypesFile(collectedKeywords, config.root);\n }\n },\n };\n};\n\nexport default keywordsPlugin;\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (code: string): Set<string> => {\n const keywords = new Set<string>();\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'ImportDeclaration' &&\n node.source.value === VIRTUAL_MODULE_ID\n ) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'index.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `declare const ${keywordConstPrefix}${key}: unique symbol;`)\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n const content = `${keywordDeclarations}\\n${exportDeclaration}\\n`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n await Promise.all(\n files.map(async (file) => {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(code);\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n }),\n );\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":["_traverse","path","module","path"]}
1
+ {"version":3,"sources":["../src/index.ts","../../minifiable-keywords/src/index.ts","../src/shared.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n buildOptions,\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n extractKeywords,\n generateModuleCode,\n generateTypesFile,\n RESOLVED_VIRTUAL_MODULE_ID,\n splitQuery,\n VIRTUAL_MODULE_ID,\n type KeywordsPluginOptions,\n type PrefixedLogger,\n} from 'minifiable-keywords';\nimport type { EnvironmentModuleGraph, Plugin, ResolvedConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nexport const keywordsPlugin = (\n options?: Partial<KeywordsPluginOptions>,\n): Plugin => {\n const pluginOptions = buildOptions(options);\n let collectedKeywords: Set<string>;\n let config: ResolvedConfig;\n let logger: PrefixedLogger;\n\n const invalidateModule = (\n absoluteId: string,\n moduleGraph: EnvironmentModuleGraph,\n ): void => {\n const module = moduleGraph.getModuleById(absoluteId);\n if (module) {\n moduleGraph.invalidateModule(module);\n module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now();\n }\n };\n\n return {\n name: PLUGIN_NAME,\n api: {\n options: pluginOptions,\n },\n\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n },\n\n async buildStart() {\n collectedKeywords = await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n pluginOptions,\n );\n },\n\n resolveId(source, importer) {\n if (!importer) {\n return;\n }\n const [validSource] = splitQuery(source);\n if (validSource === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id) {\n const [validId] = splitQuery(id);\n if (validId === RESOLVED_VIRTUAL_MODULE_ID) {\n const isDev = config.mode === 'development';\n return generateModuleCode(collectedKeywords, isDev);\n }\n },\n\n async hotUpdate({ type, file, read }) {\n if (type === 'delete') return;\n\n const fileExt = path.extname(file);\n if (\n !['.js', '.ts', '.jsx', '.tsx'].includes(fileExt) ||\n file.includes('/.')\n ) {\n return;\n }\n\n const code = await read();\n const keywordsInFile = extractKeywords(\n code,\n pluginOptions.additionalModulesToScan,\n );\n if (keywordsInFile.size === 0) return;\n\n const initialSize = collectedKeywords.size;\n for (const key of keywordsInFile) {\n collectedKeywords.add(key);\n }\n\n const newKeywordsAdded = collectedKeywords.size > initialSize;\n if (newKeywordsAdded) {\n invalidateModule(\n RESOLVED_VIRTUAL_MODULE_ID,\n this.environment.moduleGraph,\n );\n await generateTypesFile(collectedKeywords, config.root);\n }\n },\n };\n};\n\nexport default keywordsPlugin;\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface KeywordsPluginOptions {\n additionalModulesToScan: string[];\n}\n\nexport const buildOptions = (\n options: Partial<KeywordsPluginOptions> = {},\n): KeywordsPluginOptions => {\n return {\n additionalModulesToScan: options.additionalModulesToScan || [],\n };\n};\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (\n code: string,\n additionalModulesToScan: string[] = [],\n): Set<string> => {\n const keywords = new Set<string>();\n\n // Fast-path: Skip parsing if no relevant imports are present in the code.\n const containsTargetModule =\n code.includes(VIRTUAL_MODULE_ID) ||\n additionalModulesToScan.some((moduleName) => code.includes(moduleName));\n\n if (!containsTargetModule) {\n return keywords;\n }\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (node.type === 'ImportDeclaration') {\n const isTargetModule =\n node.source.value === VIRTUAL_MODULE_ID ||\n additionalModulesToScan.includes(node.source.value);\n\n if (isTargetModule) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'index.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `declare const ${keywordConstPrefix}${key}: unique symbol;`)\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n const content = `${keywordDeclarations}\\n${exportDeclaration}\\n`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n options: KeywordsPluginOptions = buildOptions(),\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n const concurrency = 100;\n for (let i = 0; i < files.length; i += concurrency) {\n const chunk = files.slice(i, i + concurrency);\n await Promise.all(\n chunk.map(async (file) => {\n try {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(\n code,\n options.additionalModulesToScan,\n );\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n } catch (error: any) {\n logger.warn(`Failed to process file ${file}: ${error.message}`);\n }\n }),\n );\n }\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n options?: KeywordsPluginOptions,\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n options,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":["_traverse","path","module","path"]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
+ import { KeywordsPluginOptions } from 'minifiable-keywords';
1
2
  import { Plugin } from 'vite';
2
3
 
3
- declare const keywordsPlugin: () => Plugin;
4
+ declare const keywordsPlugin: (options?: Partial<KeywordsPluginOptions>) => Plugin;
4
5
 
5
6
  export { keywordsPlugin as default, keywordsPlugin };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { KeywordsPluginOptions } from 'minifiable-keywords';
1
2
  import { Plugin } from 'vite';
2
3
 
3
- declare const keywordsPlugin: () => Plugin;
4
+ declare const keywordsPlugin: (options?: Partial<KeywordsPluginOptions>) => Plugin;
4
5
 
5
6
  export { keywordsPlugin as default, keywordsPlugin };
package/dist/index.js CHANGED
@@ -9,6 +9,11 @@ import _traverse from "@babel/traverse";
9
9
  import { globby } from "globby";
10
10
  var VIRTUAL_MODULE_ID = "virtual:keywords";
11
11
  var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
12
+ var buildOptions = (options = {}) => {
13
+ return {
14
+ additionalModulesToScan: options.additionalModulesToScan || []
15
+ };
16
+ };
12
17
  var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
13
18
  const prefix = usePrefix ? `[${pluginName}] ` : "";
14
19
  const prefixed = (message) => `${prefix}${message}`;
@@ -20,8 +25,12 @@ var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
20
25
  };
21
26
  };
22
27
  var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
23
- var extractKeywords = (code) => {
28
+ var extractKeywords = (code, additionalModulesToScan = []) => {
24
29
  const keywords = /* @__PURE__ */ new Set();
30
+ const containsTargetModule = code.includes(VIRTUAL_MODULE_ID) || additionalModulesToScan.some((moduleName) => code.includes(moduleName));
31
+ if (!containsTargetModule) {
32
+ return keywords;
33
+ }
25
34
  let ast;
26
35
  try {
27
36
  ast = parse(code, {
@@ -36,17 +45,20 @@ var extractKeywords = (code) => {
36
45
  traverse(ast, {
37
46
  enter(nodePath) {
38
47
  const node = nodePath.node;
39
- if (node.type === "ImportDeclaration" && node.source.value === VIRTUAL_MODULE_ID) {
40
- for (const specifier of node.specifiers) {
41
- if (specifier.type === "ImportNamespaceSpecifier") {
42
- keywordNamespaces.add(specifier.local.name);
43
- }
44
- if (specifier.type === "ImportDefaultSpecifier") {
45
- keywords.add("default");
46
- }
47
- if (specifier.type === "ImportSpecifier") {
48
- if (specifier.imported.type === "Identifier") {
49
- keywords.add(specifier.imported.name);
48
+ if (node.type === "ImportDeclaration") {
49
+ const isTargetModule = node.source.value === VIRTUAL_MODULE_ID || additionalModulesToScan.includes(node.source.value);
50
+ if (isTargetModule) {
51
+ for (const specifier of node.specifiers) {
52
+ if (specifier.type === "ImportNamespaceSpecifier") {
53
+ keywordNamespaces.add(specifier.local.name);
54
+ }
55
+ if (specifier.type === "ImportDefaultSpecifier") {
56
+ keywords.add("default");
57
+ }
58
+ if (specifier.type === "ImportSpecifier") {
59
+ if (specifier.imported.type === "Identifier") {
60
+ keywords.add(specifier.imported.name);
61
+ }
50
62
  }
51
63
  }
52
64
  }
@@ -88,7 +100,7 @@ ${exportDeclaration}
88
100
  await writeFile(path.join(pluginRoot, filename), `${content.trim()}
89
101
  `);
90
102
  };
91
- var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
103
+ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = [], options = buildOptions()) => {
92
104
  const collectedKeywords = /* @__PURE__ */ new Set();
93
105
  logger.info("Scanning project files for keywords...");
94
106
  const files = await globby("**/*.{js,ts,jsx,tsx}", {
@@ -97,25 +109,37 @@ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
97
109
  ignore: ["**/node_modules/**", ...ignoredDirs.map((dir) => `${dir}/**`)],
98
110
  gitignore: true
99
111
  });
100
- await Promise.all(
101
- files.map(async (file) => {
102
- const code = await readFile(file, "utf-8");
103
- const keywords = extractKeywords(code);
104
- for (const key of keywords) {
105
- collectedKeywords.add(key);
106
- }
107
- })
108
- );
112
+ const concurrency = 100;
113
+ for (let i = 0; i < files.length; i += concurrency) {
114
+ const chunk = files.slice(i, i + concurrency);
115
+ await Promise.all(
116
+ chunk.map(async (file) => {
117
+ try {
118
+ const code = await readFile(file, "utf-8");
119
+ const keywords = extractKeywords(
120
+ code,
121
+ options.additionalModulesToScan
122
+ );
123
+ for (const key of keywords) {
124
+ collectedKeywords.add(key);
125
+ }
126
+ } catch (error) {
127
+ logger.warn(`Failed to process file ${file}: ${error.message}`);
128
+ }
129
+ })
130
+ );
131
+ }
109
132
  logger.info(
110
133
  `Scan complete. Found ${collectedKeywords.size} unique keywords.`
111
134
  );
112
135
  return collectedKeywords;
113
136
  };
114
- var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs) => {
137
+ var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs, options) => {
115
138
  const collectedKeywords = await collectKeywordsFromFiles(
116
139
  root,
117
140
  logger,
118
- ignoredDirs
141
+ ignoredDirs,
142
+ options
119
143
  );
120
144
  await generateTypesFile(collectedKeywords, root);
121
145
  return collectedKeywords;
@@ -138,7 +162,8 @@ var splitQuery = (id) => id.split("?");
138
162
  var PLUGIN_NAME = "vite-plugin-keywords";
139
163
 
140
164
  // src/index.ts
141
- var keywordsPlugin = () => {
165
+ var keywordsPlugin = (options) => {
166
+ const pluginOptions = buildOptions(options);
142
167
  let collectedKeywords;
143
168
  let config;
144
169
  let logger;
@@ -151,6 +176,9 @@ var keywordsPlugin = () => {
151
176
  };
152
177
  return {
153
178
  name: PLUGIN_NAME,
179
+ api: {
180
+ options: pluginOptions
181
+ },
154
182
  configResolved(resolvedConfig) {
155
183
  config = resolvedConfig;
156
184
  logger = createPrefixedLogger(config.logger, PLUGIN_NAME);
@@ -159,7 +187,8 @@ var keywordsPlugin = () => {
159
187
  collectedKeywords = await collectKeywordsAndGenerateTypes(
160
188
  config.root,
161
189
  logger,
162
- [config.build.outDir, config.cacheDir]
190
+ [config.build.outDir, config.cacheDir],
191
+ pluginOptions
163
192
  );
164
193
  },
165
194
  resolveId(source, importer) {
@@ -185,7 +214,10 @@ var keywordsPlugin = () => {
185
214
  return;
186
215
  }
187
216
  const code = await read();
188
- const keywordsInFile = extractKeywords(code);
217
+ const keywordsInFile = extractKeywords(
218
+ code,
219
+ pluginOptions.additionalModulesToScan
220
+ );
189
221
  if (keywordsInFile.size === 0) return;
190
222
  const initialSize = collectedKeywords.size;
191
223
  for (const key of keywordsInFile) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../minifiable-keywords/src/index.ts","../src/shared.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n extractKeywords,\n generateModuleCode,\n generateTypesFile,\n RESOLVED_VIRTUAL_MODULE_ID,\n splitQuery,\n VIRTUAL_MODULE_ID,\n type PrefixedLogger,\n} from 'minifiable-keywords';\nimport type { EnvironmentModuleGraph, Plugin, ResolvedConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nexport const keywordsPlugin = (): Plugin => {\n let collectedKeywords: Set<string>;\n let config: ResolvedConfig;\n let logger: PrefixedLogger;\n\n const invalidateModule = (\n absoluteId: string,\n moduleGraph: EnvironmentModuleGraph,\n ): void => {\n const module = moduleGraph.getModuleById(absoluteId);\n if (module) {\n moduleGraph.invalidateModule(module);\n module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now();\n }\n };\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n },\n\n async buildStart() {\n collectedKeywords = await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n );\n },\n\n resolveId(source, importer) {\n if (!importer) {\n return;\n }\n const [validSource] = splitQuery(source);\n if (validSource === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id) {\n const [validId] = splitQuery(id);\n if (validId === RESOLVED_VIRTUAL_MODULE_ID) {\n const isDev = config.mode === 'development';\n return generateModuleCode(collectedKeywords, isDev);\n }\n },\n\n async hotUpdate({ type, file, read }) {\n if (type === 'delete') return;\n\n const fileExt = path.extname(file);\n if (\n !['.js', '.ts', '.jsx', '.tsx'].includes(fileExt) ||\n file.includes('/.')\n ) {\n return;\n }\n\n const code = await read();\n const keywordsInFile = extractKeywords(code);\n if (keywordsInFile.size === 0) return;\n\n const initialSize = collectedKeywords.size;\n for (const key of keywordsInFile) {\n collectedKeywords.add(key);\n }\n\n const newKeywordsAdded = collectedKeywords.size > initialSize;\n if (newKeywordsAdded) {\n invalidateModule(\n RESOLVED_VIRTUAL_MODULE_ID,\n this.environment.moduleGraph,\n );\n await generateTypesFile(collectedKeywords, config.root);\n }\n },\n };\n};\n\nexport default keywordsPlugin;\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (code: string): Set<string> => {\n const keywords = new Set<string>();\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'ImportDeclaration' &&\n node.source.value === VIRTUAL_MODULE_ID\n ) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'index.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `declare const ${keywordConstPrefix}${key}: unique symbol;`)\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n const content = `${keywordDeclarations}\\n${exportDeclaration}\\n`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n await Promise.all(\n files.map(async (file) => {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(code);\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n }),\n );\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":["path","path"]}
1
+ {"version":3,"sources":["../src/index.ts","../../minifiable-keywords/src/index.ts","../src/shared.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n buildOptions,\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n extractKeywords,\n generateModuleCode,\n generateTypesFile,\n RESOLVED_VIRTUAL_MODULE_ID,\n splitQuery,\n VIRTUAL_MODULE_ID,\n type KeywordsPluginOptions,\n type PrefixedLogger,\n} from 'minifiable-keywords';\nimport type { EnvironmentModuleGraph, Plugin, ResolvedConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nexport const keywordsPlugin = (\n options?: Partial<KeywordsPluginOptions>,\n): Plugin => {\n const pluginOptions = buildOptions(options);\n let collectedKeywords: Set<string>;\n let config: ResolvedConfig;\n let logger: PrefixedLogger;\n\n const invalidateModule = (\n absoluteId: string,\n moduleGraph: EnvironmentModuleGraph,\n ): void => {\n const module = moduleGraph.getModuleById(absoluteId);\n if (module) {\n moduleGraph.invalidateModule(module);\n module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now();\n }\n };\n\n return {\n name: PLUGIN_NAME,\n api: {\n options: pluginOptions,\n },\n\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n },\n\n async buildStart() {\n collectedKeywords = await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n pluginOptions,\n );\n },\n\n resolveId(source, importer) {\n if (!importer) {\n return;\n }\n const [validSource] = splitQuery(source);\n if (validSource === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id) {\n const [validId] = splitQuery(id);\n if (validId === RESOLVED_VIRTUAL_MODULE_ID) {\n const isDev = config.mode === 'development';\n return generateModuleCode(collectedKeywords, isDev);\n }\n },\n\n async hotUpdate({ type, file, read }) {\n if (type === 'delete') return;\n\n const fileExt = path.extname(file);\n if (\n !['.js', '.ts', '.jsx', '.tsx'].includes(fileExt) ||\n file.includes('/.')\n ) {\n return;\n }\n\n const code = await read();\n const keywordsInFile = extractKeywords(\n code,\n pluginOptions.additionalModulesToScan,\n );\n if (keywordsInFile.size === 0) return;\n\n const initialSize = collectedKeywords.size;\n for (const key of keywordsInFile) {\n collectedKeywords.add(key);\n }\n\n const newKeywordsAdded = collectedKeywords.size > initialSize;\n if (newKeywordsAdded) {\n invalidateModule(\n RESOLVED_VIRTUAL_MODULE_ID,\n this.environment.moduleGraph,\n );\n await generateTypesFile(collectedKeywords, config.root);\n }\n },\n };\n};\n\nexport default keywordsPlugin;\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface KeywordsPluginOptions {\n additionalModulesToScan: string[];\n}\n\nexport const buildOptions = (\n options: Partial<KeywordsPluginOptions> = {},\n): KeywordsPluginOptions => {\n return {\n additionalModulesToScan: options.additionalModulesToScan || [],\n };\n};\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (\n code: string,\n additionalModulesToScan: string[] = [],\n): Set<string> => {\n const keywords = new Set<string>();\n\n // Fast-path: Skip parsing if no relevant imports are present in the code.\n const containsTargetModule =\n code.includes(VIRTUAL_MODULE_ID) ||\n additionalModulesToScan.some((moduleName) => code.includes(moduleName));\n\n if (!containsTargetModule) {\n return keywords;\n }\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (node.type === 'ImportDeclaration') {\n const isTargetModule =\n node.source.value === VIRTUAL_MODULE_ID ||\n additionalModulesToScan.includes(node.source.value);\n\n if (isTargetModule) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'index.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `declare const ${keywordConstPrefix}${key}: unique symbol;`)\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n const content = `${keywordDeclarations}\\n${exportDeclaration}\\n`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n options: KeywordsPluginOptions = buildOptions(),\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n const concurrency = 100;\n for (let i = 0; i < files.length; i += concurrency) {\n const chunk = files.slice(i, i + concurrency);\n await Promise.all(\n chunk.map(async (file) => {\n try {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(\n code,\n options.additionalModulesToScan,\n );\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n } catch (error: any) {\n logger.warn(`Failed to process file ${file}: ${error.message}`);\n }\n }),\n );\n }\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n options?: KeywordsPluginOptions,\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n options,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":["path","path"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-keywords",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A Vite plugin that provides minifiable Symbols (keywords) to use in place of string literals for aggressive minification/obfuscation.",
5
5
  "keywords": [
6
6
  "vite",
@@ -46,7 +46,7 @@
46
46
  "rimraf": "^6.0.1",
47
47
  "tsup": "^8.5.0",
48
48
  "vitest": "^4.0.0",
49
- "minifiable-keywords": "1.1.1"
49
+ "minifiable-keywords": "1.2.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "vite": "^6.0.0 || ^7.0.0"