jsrepo 1.0.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.
@@ -0,0 +1,198 @@
1
+ import fs from 'node:fs';
2
+ import { builtinModules } from 'node:module';
3
+ import path from 'node:path';
4
+ import { walk } from 'estree-walker';
5
+ import * as sv from 'svelte/compiler';
6
+ import { Project } from 'ts-morph';
7
+ import validatePackageName from 'validate-npm-package-name';
8
+ import { Err, Ok, type Result } from '../blocks/types/result';
9
+ import { findNearestPackageJson } from './package';
10
+
11
+ export type ResolvedDependencies = {
12
+ local: string[];
13
+ devDependencies: string[];
14
+ dependencies: string[];
15
+ };
16
+
17
+ export type Lang = {
18
+ /** Matches the supported file types */
19
+ matches: (fileName: string) => boolean;
20
+ /** Reads the file and gets any dependencies from its imports */
21
+ resolveDependencies: (
22
+ filePath: string,
23
+ category: string,
24
+ isSubDir: boolean
25
+ ) => Result<ResolvedDependencies, string>;
26
+ /** Returns a multiline comment containing the content */
27
+ comment: (content: string) => string;
28
+ };
29
+
30
+ const typescript: Lang = {
31
+ matches: (fileName) =>
32
+ fileName.endsWith('.ts') ||
33
+ fileName.endsWith('.js') ||
34
+ fileName.endsWith('.tsx') ||
35
+ fileName.endsWith('.jsx'),
36
+ resolveDependencies: (filePath, category, isSubDir) => {
37
+ const project = new Project();
38
+
39
+ const blockFile = project.addSourceFileAtPath(filePath);
40
+
41
+ const imports = blockFile.getImportDeclarations();
42
+
43
+ const relativeImports = imports.filter((declaration) =>
44
+ declaration.getModuleSpecifierValue().startsWith('.')
45
+ );
46
+
47
+ const localDeps = new Set<string>();
48
+
49
+ for (const relativeImport of relativeImports) {
50
+ const mod = relativeImport.getModuleSpecifierValue();
51
+
52
+ const localDep = resolveLocalImport(mod, category, isSubDir);
53
+
54
+ if (localDep) localDeps.add(localDep);
55
+ }
56
+
57
+ const deps = imports
58
+ .filter((declaration) => !declaration.getModuleSpecifierValue().startsWith('.'))
59
+ .map((declaration) => declaration.getModuleSpecifierValue());
60
+
61
+ const { devDependencies, dependencies } = resolveRemoteDeps(Array.from(deps), filePath);
62
+
63
+ return Ok({
64
+ local: Array.from(localDeps),
65
+ dependencies,
66
+ devDependencies,
67
+ } satisfies ResolvedDependencies);
68
+ },
69
+ comment: (content) => `/*\n${content}\n*/`,
70
+ };
71
+
72
+ const svelte: Lang = {
73
+ matches: (fileName) => fileName.endsWith('.svelte'),
74
+ resolveDependencies: (filePath, category, isSubDir) => {
75
+ const sourceCode = fs.readFileSync(filePath).toString();
76
+
77
+ const root = sv.parse(sourceCode, { modern: true });
78
+
79
+ // if no script tag then no dependencies
80
+ if (!root.instance) return Ok({ dependencies: [], devDependencies: [], local: [] });
81
+
82
+ const localDeps = new Set<string>();
83
+ const deps = new Set<string>();
84
+
85
+ // biome-ignore lint/suspicious/noExplicitAny: The root instance is just missing the `id` prop
86
+ walk(root.instance as any, {
87
+ enter: (node) => {
88
+ if (node.type === 'ImportDeclaration') {
89
+ if (typeof node.source.value === 'string') {
90
+ if (node.source.value.startsWith('.')) {
91
+ const localDep = resolveLocalImport(
92
+ node.source.value,
93
+ category,
94
+ isSubDir
95
+ );
96
+
97
+ if (localDep) localDeps.add(localDep);
98
+ } else {
99
+ deps.add(node.source.value);
100
+ }
101
+ }
102
+ }
103
+ },
104
+ });
105
+
106
+ const { devDependencies, dependencies } = resolveRemoteDeps(Array.from(deps), filePath);
107
+
108
+ return Ok({
109
+ dependencies,
110
+ devDependencies,
111
+ local: Array.from(localDeps),
112
+ } satisfies ResolvedDependencies);
113
+ },
114
+ comment: (content) => `<!--\n${content}\n-->`,
115
+ };
116
+
117
+ const resolveLocalImport = (
118
+ mod: string,
119
+ category: string,
120
+ isSubDir: boolean
121
+ ): string | undefined => {
122
+ // do not add local deps that are within the same folder
123
+ if (isSubDir && mod.startsWith('./')) return undefined;
124
+
125
+ if (mod.startsWith('./')) {
126
+ return `${category}/${path.parse(path.basename(mod)).name}`;
127
+ }
128
+
129
+ if (isSubDir && mod.startsWith('../') && !mod.startsWith('../.')) {
130
+ return `${category}/${path.parse(path.basename(mod)).name}`;
131
+ }
132
+
133
+ const segments = mod.replaceAll('../', '').split('/');
134
+
135
+ // invalid path
136
+ if (segments.length !== 2) return undefined;
137
+
138
+ return `${segments[0]}/${segments[1]}`;
139
+ };
140
+
141
+ /** Iterates over the dependency and resolves each one using the nearest package.json file.
142
+ * Strips node APIs and pins the version of each dependency based on what is in the package.json.
143
+ *
144
+ * @param deps
145
+ * @param filePath
146
+ * @returns
147
+ */
148
+ const resolveRemoteDeps = (deps: string[], filePath: string) => {
149
+ const filteredDeps = deps.filter(
150
+ (dep) =>
151
+ !builtinModules.includes(dep) &&
152
+ !dep.startsWith('node:') &&
153
+ validatePackageName(dep).validForNewPackages
154
+ );
155
+
156
+ const pkgPath = findNearestPackageJson(path.dirname(filePath), '');
157
+
158
+ const dependencies = new Set<string>();
159
+ const devDependencies = new Set<string>();
160
+
161
+ if (pkgPath) {
162
+ const { devDependencies: packageDevDependencies, dependencies: packageDependencies } =
163
+ JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
164
+
165
+ for (const dep of filteredDeps) {
166
+ let version: string | undefined = undefined;
167
+ if (packageDependencies !== undefined) {
168
+ version = packageDependencies[dep];
169
+ }
170
+
171
+ if (version !== undefined) {
172
+ dependencies.add(`${dep}@${version}`);
173
+ continue;
174
+ }
175
+
176
+ if (packageDevDependencies !== undefined) {
177
+ version = packageDevDependencies[dep];
178
+ }
179
+
180
+ if (version !== undefined) {
181
+ devDependencies.add(`${dep}@${version}`);
182
+ continue;
183
+ }
184
+
185
+ // if no version found just add it without a version
186
+ dependencies.add(dep);
187
+ }
188
+ }
189
+
190
+ return {
191
+ dependencies: Array.from(dependencies),
192
+ devDependencies: Array.from(devDependencies),
193
+ };
194
+ };
195
+
196
+ const languages: Lang[] = [typescript, svelte];
197
+
198
+ export { typescript, languages };
@@ -0,0 +1,16 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const findNearestPackageJson = (startDir: string, until: string): string | undefined => {
5
+ const packagePath = path.join(startDir, 'package.json');
6
+
7
+ if (fs.existsSync(packagePath)) return packagePath;
8
+
9
+ if (startDir === until) return undefined;
10
+
11
+ const segments = startDir.split(/[\/\\]/);
12
+
13
+ return findNearestPackageJson(segments.slice(0, segments.length - 1).join('/'), until);
14
+ };
15
+
16
+ export { findNearestPackageJson };
@@ -0,0 +1,72 @@
1
+ import { intro, spinner } from '@clack/prompts';
2
+ import color from 'chalk';
3
+ import { rightPad, rightPadMin } from '../blocks/utilities/pad';
4
+ import { stripAsni } from '../blocks/utilities/strip-ansi';
5
+
6
+ const VERTICAL_BORDER = color.gray('│');
7
+ const HORIZONTAL_BORDER = color.gray('─');
8
+ const TOP_RIGHT_CORNER = color.gray('┐');
9
+ const BOTTOM_RIGHT_CORNER = color.gray('┘');
10
+ const JUNCTION_RIGHT = color.gray('├');
11
+ // we may need these eventually
12
+ // const TOP_LEFT_CORNER = color.gray("┌");
13
+ // const BOTTOM_LEFT_CORNER = color.gray("└");
14
+
15
+ export type Task = {
16
+ loadingMessage: string;
17
+ completedMessage: string;
18
+ run: () => Promise<void>;
19
+ };
20
+
21
+ const runTasks = async (tasks: Task[], { verbose = false }) => {
22
+ const loading = spinner();
23
+
24
+ for (const task of tasks) {
25
+ // we don't want this to clear logs when in verbose mode
26
+ if (!verbose) loading.start(task.loadingMessage);
27
+
28
+ try {
29
+ await task.run();
30
+ } catch (err) {
31
+ console.error(err);
32
+ }
33
+
34
+ if (!verbose) loading.stop(task.completedMessage);
35
+ }
36
+ };
37
+
38
+ const nextSteps = (steps: string[]): string => {
39
+ let max = 20;
40
+ steps.map((val) => {
41
+ const reset = rightPad(stripAsni(val), 4);
42
+
43
+ if (reset.length > max) max = reset.length;
44
+ });
45
+
46
+ const NEXT_STEPS = 'Next Steps';
47
+
48
+ let result = `${VERTICAL_BORDER}\n`;
49
+
50
+ // top
51
+ result += `${JUNCTION_RIGHT} ${NEXT_STEPS} ${HORIZONTAL_BORDER.repeat(
52
+ max - NEXT_STEPS.length - 1
53
+ )}${TOP_RIGHT_CORNER}\n`;
54
+
55
+ result += `${VERTICAL_BORDER} ${' '.repeat(max)} ${VERTICAL_BORDER}\n`;
56
+
57
+ steps.map((step) => {
58
+ result += `${VERTICAL_BORDER} ${rightPadMin(step, max - 1)} ${VERTICAL_BORDER}\n`;
59
+ });
60
+
61
+ result += `${VERTICAL_BORDER} ${' '.repeat(max)} ${VERTICAL_BORDER}\n`;
62
+
63
+ // bottom
64
+ result += `${JUNCTION_RIGHT}${HORIZONTAL_BORDER.repeat(max + 2)}${BOTTOM_RIGHT_CORNER}\n`;
65
+
66
+ return result;
67
+ };
68
+
69
+ const _intro = (version: string) =>
70
+ intro(`${color.bgHex('#f7df1e').black(' jsrepo ')}${color.gray(` v${version} `)}`);
71
+
72
+ export { runTasks, nextSteps, _intro as intro };