Skip to content

Commit 690c2fe

Browse files
local packages path
1 parent 4558e99 commit 690c2fe

File tree

9 files changed

+209
-29
lines changed

9 files changed

+209
-29
lines changed

packages/cli/src/commands/bundle/bundler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export async function bundleCore(
138138
([name, packageConfig]) => ({
139139
name,
140140
version: packageConfig.version || 'latest',
141+
path: packageConfig.path, // Pass local path if defined
141142
}),
142143
);
143144
// downloadPackages adds 'node_modules' subdirectory automatically
@@ -146,6 +147,7 @@ export async function bundleCore(
146147
TEMP_DIR,
147148
logger,
148149
buildOptions.cache,
150+
buildOptions.configDir, // For resolving relative local paths
149151
);
150152

151153
// Fix @walkeros packages to have proper ESM exports

packages/cli/src/commands/bundle/package-manager.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import pacote from 'pacote';
22
import path from 'path';
33
import fs from 'fs-extra';
4-
import { Logger } from '../../core/index.js';
4+
import {
5+
Logger,
6+
resolveLocalPackage,
7+
copyLocalPackage,
8+
} from '../../core/index.js';
59
import { getPackageCacheKey } from '../../core/cache-utils.js';
610

711
export interface Package {
812
name: string;
913
version: string;
14+
path?: string; // Local path to package directory
1015
}
1116

1217
/**
@@ -123,6 +128,7 @@ export async function downloadPackages(
123128
targetDir: string,
124129
logger: Logger,
125130
useCache = true,
131+
configDir?: string, // For resolving relative local paths
126132
): Promise<Map<string, string>> {
127133
const packagePaths = new Map<string, string>();
128134
const downloadQueue: Package[] = [...packages];
@@ -142,6 +148,29 @@ export async function downloadPackages(
142148
continue;
143149
}
144150
processed.add(pkgKey);
151+
152+
// Handle local packages first
153+
if (pkg.path) {
154+
const localPkg = await resolveLocalPackage(
155+
pkg.name,
156+
pkg.path,
157+
configDir || process.cwd(),
158+
logger,
159+
);
160+
const installedPath = await copyLocalPackage(localPkg, targetDir, logger);
161+
packagePaths.set(pkg.name, installedPath);
162+
163+
// Resolve dependencies from local package
164+
const deps = await resolveDependencies(pkg, installedPath, logger);
165+
for (const dep of deps) {
166+
const depKey = `${dep.name}@${dep.version}`;
167+
if (!processed.has(depKey)) {
168+
downloadQueue.push(dep);
169+
}
170+
}
171+
continue;
172+
}
173+
145174
const packageSpec = `${pkg.name}@${pkg.version}`;
146175
// Use proper node_modules structure: node_modules/@scope/package
147176
const packageDir = getPackageDirectory(targetDir, pkg.name, pkg.version);

packages/cli/src/commands/simulate/simulator.ts

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
type BuildOptions,
1212
} from '../../config/index.js';
1313
import { bundleCore } from '../bundle/bundler.js';
14-
import { downloadPackages } from '../bundle/package-manager.js';
1514
import { CallTracker } from './tracker.js';
1615
import { executeInJSDOM } from './jsdom-executor.js';
1716
import { executeInNode } from './node-executor.js';
@@ -131,31 +130,10 @@ export async function executeSimulation(
131130
// Detect platform from flowConfig
132131
const platform = getPlatform(flowConfig);
133132

134-
// 2. Download packages to temp directory
135-
// This ensures we use clean npm packages, not workspace packages
136-
const packagesArray = Object.entries(buildOptions.packages).map(
137-
([name, packageConfig]) => ({
138-
name,
139-
version:
140-
(typeof packageConfig === 'object' &&
141-
packageConfig !== null &&
142-
'version' in packageConfig &&
143-
typeof packageConfig.version === 'string'
144-
? packageConfig.version
145-
: undefined) || 'latest',
146-
}),
147-
);
148-
const packagePaths = await downloadPackages(
149-
packagesArray,
150-
tempDir, // downloadPackages will add 'node_modules' subdirectory itself
151-
createLogger({ silent: true }),
152-
buildOptions.cache,
153-
);
154-
155-
// 3. Create tracker
133+
// 2. Create tracker
156134
const tracker = new CallTracker();
157135

158-
// 4. Create temporary bundle
136+
// 3. Create temporary bundle
159137
const tempOutput = path.join(
160138
tempDir,
161139
`simulation-bundle-${generateId()}.${platform === 'web' ? 'js' : 'mjs'}`,
@@ -184,7 +162,7 @@ export async function executeSimulation(
184162
}),
185163
};
186164

187-
// 5. Bundle with downloaded packages (they're already in tempDir/node_modules)
165+
// 4. Bundle (downloads packages internally)
188166
await bundleCore(
189167
flowConfig,
190168
simulationBuildOptions,
@@ -193,10 +171,10 @@ export async function executeSimulation(
193171
);
194172
bundlePath = tempOutput;
195173

196-
// 6. Load env examples dynamically from destination packages
174+
// 5. Load env examples dynamically from destination packages
197175
const envs = await loadDestinationEnvs(destinations || {});
198176

199-
// 7. Execute based on platform
177+
// 6. Execute based on platform
200178
let result;
201179
if (platform === 'web') {
202180
result = await executeInJSDOM(

packages/cli/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './docker.js';
66
export * from './temp-manager.js';
77
export * from './asset-resolver.js';
88
export * from './utils.js';
9+
export * from './local-packages.js';
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import path from 'path';
2+
import fs from 'fs-extra';
3+
import type { Logger } from './logger.js';
4+
5+
export interface LocalPackageInfo {
6+
name: string;
7+
absolutePath: string;
8+
distPath: string;
9+
hasDistFolder: boolean;
10+
}
11+
12+
/**
13+
* Resolve and validate a local package path
14+
*/
15+
export async function resolveLocalPackage(
16+
packageName: string,
17+
localPath: string,
18+
configDir: string,
19+
logger: Logger,
20+
): Promise<LocalPackageInfo> {
21+
// Resolve relative to config file directory
22+
const absolutePath = path.isAbsolute(localPath)
23+
? localPath
24+
: path.resolve(configDir, localPath);
25+
26+
// Validate path exists
27+
if (!(await fs.pathExists(absolutePath))) {
28+
throw new Error(
29+
`Local package path not found: ${localPath} (resolved to ${absolutePath})`,
30+
);
31+
}
32+
33+
// Validate package.json exists
34+
const pkgJsonPath = path.join(absolutePath, 'package.json');
35+
if (!(await fs.pathExists(pkgJsonPath))) {
36+
throw new Error(
37+
`No package.json found at ${absolutePath}. Is this a valid package directory?`,
38+
);
39+
}
40+
41+
// Check for dist folder
42+
const distPath = path.join(absolutePath, 'dist');
43+
const hasDistFolder = await fs.pathExists(distPath);
44+
45+
if (!hasDistFolder) {
46+
logger.warn(
47+
`⚠️ ${packageName}: No dist/ folder found. Using package root.`,
48+
);
49+
}
50+
51+
return {
52+
name: packageName,
53+
absolutePath,
54+
distPath: hasDistFolder ? distPath : absolutePath,
55+
hasDistFolder,
56+
};
57+
}
58+
59+
/**
60+
* Copy local package to target node_modules directory
61+
*/
62+
export async function copyLocalPackage(
63+
localPkg: LocalPackageInfo,
64+
targetDir: string,
65+
logger: Logger,
66+
): Promise<string> {
67+
const packageDir = path.join(targetDir, 'node_modules', localPkg.name);
68+
69+
await fs.ensureDir(path.dirname(packageDir));
70+
71+
// Copy dist folder contents (or package root if no dist)
72+
await fs.copy(localPkg.distPath, packageDir);
73+
74+
// Always copy package.json for module resolution
75+
await fs.copy(
76+
path.join(localPkg.absolutePath, 'package.json'),
77+
path.join(packageDir, 'package.json'),
78+
);
79+
80+
logger.info(`📦 Using local: ${localPkg.name} from ${localPkg.absolutePath}`);
81+
82+
return packageDir;
83+
}

packages/core/src/schemas/flow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const PackagesSchema = z
5858
z.object({
5959
version: z.string().optional(),
6060
imports: z.array(z.string()).optional(),
61+
path: z.string().optional(), // Local path (takes precedence over version)
6162
}),
6263
)
6364
.describe('NPM packages to bundle');

packages/core/src/types/flow.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ export type Definitions = Record<string, unknown>;
3434
/**
3535
* Packages configuration for build.
3636
*/
37-
export type Packages = Record<string, { version?: string; imports?: string[] }>;
37+
export type Packages = Record<
38+
string,
39+
{
40+
version?: string;
41+
imports?: string[];
42+
path?: string; // Local path to package directory (takes precedence over version)
43+
}
44+
>;
3845

3946
/**
4047
* Web platform configuration.

website/docs/apps/cli.mdx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,68 @@ The CLI uses types from `@walkeros/core`:
5858

5959
The CLI transforms `Flow.Setup``Flow.Config` (per flow) → bundled code that uses `Collector.InitConfig` at runtime.
6060

61+
## Local Packages
62+
63+
By default, the CLI downloads packages from npm. For development or testing unpublished packages, you can use local packages instead by specifying a `path` property.
64+
65+
### Configuration
66+
67+
Add a `path` property to any package to use a local directory instead of npm:
68+
69+
<CodeSnippet
70+
code={`{
71+
"packages": {
72+
"@walkeros/collector": {
73+
"version": "latest",
74+
"imports": ["startFlow"]
75+
},
76+
"@my/custom-destination": {
77+
"path": "./my-custom-destination",
78+
"imports": ["myDestination"]
79+
}
80+
}
81+
}`}
82+
language="json"
83+
/>
84+
85+
### Resolution Rules
86+
87+
- **`path` takes precedence** - When both `path` and `version` are specified, `path` is used
88+
- **Relative paths** - Resolved relative to the config file's directory
89+
- **Absolute paths** - Used as-is
90+
- **dist folder** - If a `dist/` folder exists, it's used; otherwise the package root is used
91+
92+
### Use Cases
93+
94+
**Development of custom packages:**
95+
<CodeSnippet
96+
code={`{
97+
"packages": {
98+
"@my-org/destination-custom": {
99+
"path": "../my-destination",
100+
"imports": ["destinationCustom"]
101+
}
102+
}
103+
}`}
104+
language="json"
105+
/>
106+
107+
**Testing local changes to walkerOS packages:**
108+
<CodeSnippet
109+
code={`{
110+
"packages": {
111+
"@walkeros/collector": {
112+
"version": "latest",
113+
"path": "../../packages/collector",
114+
"imports": ["startFlow"]
115+
}
116+
}
117+
}`}
118+
language="json"
119+
/>
120+
121+
When ready for production, simply remove the `path` property to use the published npm version.
122+
61123
## Getting Started
62124

63125
Before using the CLI, you need a [flow configuration file](/docs/getting-started/flow). Here's a minimal example:

website/docs/getting-started/flow.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,23 @@ Specifies npm packages to download and bundle:
135135
**Properties:**
136136
- **`version`** - npm version (semver or "latest", defaults to "latest")
137137
- **`imports`** - Array of named exports to import
138+
- **`path`** - Local filesystem path (takes precedence over `version`)
139+
140+
For development or custom packages, use `path` to reference a local directory:
141+
142+
<CodeSnippet
143+
code={`{
144+
"packages": {
145+
"@my/custom-destination": {
146+
"path": "./my-destination",
147+
"imports": ["myDestination"]
148+
}
149+
}
150+
}`}
151+
language="json"
152+
/>
153+
154+
See [Local Packages](/docs/apps/cli#local-packages) in the CLI documentation for more details.
138155

139156
### Sources
140157

0 commit comments

Comments
 (0)