diff --git a/packages/core/core/src/Parcel.js b/packages/core/core/src/Parcel.js index 9adff5dccd7..028110838eb 100644 --- a/packages/core/core/src/Parcel.js +++ b/packages/core/core/src/Parcel.js @@ -1,12 +1,17 @@ // @flow strict-local import type { + Asset, AsyncSubscription, BuildEvent, BuildSuccessEvent, InitialParcelOptions, PackagedBundle as IPackagedBundle, + ParcelTransformOptions, + ParcelResolveOptions, + ParcelResolveResult, } from '@parcel/types'; +import path from 'path'; import type {ParcelOptions} from './types'; // eslint-disable-next-line no-unused-vars import type {FarmOptions, SharedReference} from '@parcel/workers'; @@ -37,10 +42,18 @@ import RequestTracker, { } from './RequestTracker'; import createValidationRequest from './requests/ValidationRequest'; import createParcelBuildRequest from './requests/ParcelBuildRequest'; +import createAssetRequest from './requests/AssetRequest'; +import createPathRequest from './requests/PathRequest'; +import {createEnvironment} from './Environment'; +import {createDependency} from './Dependency'; import {Disposable} from '@parcel/events'; import {init as initSourcemaps} from '@parcel/source-map'; import {init as initHash} from '@parcel/hash'; -import {toProjectPath} from './projectPath'; +import { + fromProjectPath, + toProjectPath, + fromProjectPathRelative, +} from './projectPath'; import {tracer} from '@parcel/profiler'; registerCoreWithSerializer(); @@ -437,6 +450,86 @@ export default class Parcel { logger.info({origin: '@parcel/core', message: 'Taking heap snapshot...'}); return this.#farm.takeHeapSnapshot(); } + + async unstable_transform( + options: ParcelTransformOptions, + ): Promise> { + if (!this.#initialized) { + await this._init(); + } + + let projectRoot = nullthrows(this.#resolvedOptions).projectRoot; + let request = createAssetRequest({ + ...options, + filePath: toProjectPath(projectRoot, options.filePath), + optionsRef: this.#optionsRef, + env: createEnvironment({ + ...options.env, + loc: + options.env?.loc != null + ? { + ...options.env.loc, + filePath: toProjectPath(projectRoot, options.env.loc.filePath), + } + : undefined, + }), + }); + + let res = await this.#requestTracker.runRequest(request, { + force: true, + }); + return res.map(asset => + assetFromValue(asset, nullthrows(this.#resolvedOptions)), + ); + } + + async unstable_resolve( + request: ParcelResolveOptions, + ): Promise { + if (!this.#initialized) { + await this._init(); + } + + let projectRoot = nullthrows(this.#resolvedOptions).projectRoot; + if (request.resolveFrom == null && path.isAbsolute(request.specifier)) { + request.specifier = fromProjectPathRelative( + toProjectPath(projectRoot, request.specifier), + ); + } + + let dependency = createDependency(projectRoot, { + ...request, + env: createEnvironment({ + ...request.env, + loc: + request.env?.loc != null + ? { + ...request.env.loc, + filePath: toProjectPath(projectRoot, request.env.loc.filePath), + } + : undefined, + }), + }); + + let req = createPathRequest({ + dependency, + name: request.specifier, + }); + + let res = await this.#requestTracker.runRequest(req, { + force: true, + }); + if (!res) { + return null; + } + + return { + filePath: fromProjectPath(projectRoot, res.filePath), + code: res.code, + query: res.query, + sideEffects: res.sideEffects, + }; + } } export class BuildError extends ThrowableDiagnostic { diff --git a/packages/core/core/test/Parcel.test.js b/packages/core/core/test/Parcel.test.js index 2419cbaef9e..36b2681a5e0 100644 --- a/packages/core/core/test/Parcel.test.js +++ b/packages/core/core/test/Parcel.test.js @@ -75,6 +75,59 @@ describe('Parcel', function () { }); }); +describe('ParcelAPI', function () { + this.timeout(75000); + + let workerFarm; + beforeEach(() => { + workerFarm = createWorkerFarm(); + }); + + afterEach(() => workerFarm.end()); + + describe('parcel.unstable_transform()', () => { + it('should transforms simple file', async () => { + let parcel = createParcel({workerFarm}); + let res = await parcel.unstable_transform({ + filePath: path.join(__dirname, 'fixtures/parcel/index.js'), + }); + let code = await res[0].getCode(); + assert(code.includes('exports.default = "test"')); + }); + + it('should transform with standalone mode', async () => { + let parcel = createParcel({workerFarm}); + let res = await parcel.unstable_transform({ + filePath: path.join(__dirname, 'fixtures/parcel/other.js'), + query: 'standalone=true', + }); + let code = await res[0].getCode(); + + assert(code.includes('require("./index.js")')); + assert(code.includes('new URL("index.js", "file:" + __filename);')); + assert(code.includes('import("index.js")')); + }); + }); + + describe('parcel.resolve()', () => { + it('should resolve dependencies', async () => { + let parcel = createParcel({workerFarm}); + let res = await parcel.unstable_resolve({ + specifier: './other', + specifierType: 'esm', + resolveFrom: path.join(__dirname, 'fixtures/parcel/index.js'), + }); + + assert.deepEqual(res, { + filePath: path.join(__dirname, 'fixtures/parcel/other.js'), + code: undefined, + query: undefined, + sideEffects: true, + }); + }); + }); +}); + function createParcel(opts?: InitialParcelOptions) { return new Parcel({ entries: [path.join(__dirname, 'fixtures/parcel/index.js')], diff --git a/packages/core/core/test/fixtures/parcel/index.js b/packages/core/core/test/fixtures/parcel/index.js index e69de29bb2d..2ea084092a2 100644 --- a/packages/core/core/test/fixtures/parcel/index.js +++ b/packages/core/core/test/fixtures/parcel/index.js @@ -0,0 +1 @@ +export default 'test'; diff --git a/packages/core/core/test/fixtures/parcel/other.js b/packages/core/core/test/fixtures/parcel/other.js new file mode 100644 index 00000000000..fc7ecc15c5b --- /dev/null +++ b/packages/core/core/test/fixtures/parcel/other.js @@ -0,0 +1,3 @@ +import * as idx from './index.js'; +new URL('index.js', import.meta.url); +import('index.js') diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 7a496997678..31728405c28 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -648,6 +648,27 @@ export type ASTGenerator = {| export type BundleBehavior = 'inline' | 'isolated'; +export type ParcelTransformOptions = {| + filePath: FilePath, + code?: string, + env?: EnvironmentOptions, + query?: ?string, +|}; + +export type ParcelResolveOptions = {| + specifier: DependencySpecifier, + specifierType: SpecifierType, + env?: EnvironmentOptions, + resolveFrom?: FilePath, +|}; + +export type ParcelResolveResult = {| + filePath: FilePath, + code?: string, + query?: ?string, + sideEffects?: boolean, +|}; + /** * An asset represents a file or part of a file. It may represent any data type, including source code, * binary data, etc. Assets may exist in the file system or may be virtual. diff --git a/packages/transformers/js/core/src/dependency_collector.rs b/packages/transformers/js/core/src/dependency_collector.rs index 0650ca3fca1..2d268e29b31 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -125,15 +125,16 @@ impl<'a> DependencyCollector<'a> { None } } - _ => Some(format!( + _ if !self.config.standalone => Some(format!( "{:x}", hash!(format!( "{}:{}:{}", self.get_project_relative_filename(), specifier, kind - )) + )), )), + _ => None, }; self.items.push(DependencyDescriptor { @@ -158,7 +159,7 @@ impl<'a> DependencyCollector<'a> { source_type: SourceType, ) -> ast::Expr { // If not a library, replace with a require call pointing to a runtime that will resolve the url dynamically. - if !self.config.is_library { + if !self.config.is_library && !self.config.standalone { let placeholder = self.add_dependency(specifier.clone(), span, kind, None, false, source_type); let specifier = if let Some(placeholder) = placeholder { @@ -172,13 +173,17 @@ impl<'a> DependencyCollector<'a> { // For library builds, we need to create something that can be statically analyzed by another bundler, // so rather than replacing with a require call that is resolved by a runtime, replace with a `new URL` // call with a placeholder for the relative path to be replaced during packaging. - let placeholder = format!( - "{:x}", - hash!(format!( - "parcel_url:{}:{}:{}", - self.config.filename, specifier, kind - )) - ); + let placeholder = if self.config.standalone { + specifier.as_ref().into() + } else { + format!( + "{:x}", + hash!(format!( + "parcel_url:{}:{}:{}", + self.config.filename, specifier, kind + )) + ) + }; self.items.push(DependencyDescriptor { kind, loc: SourceLocation::from(self.source_map, span), @@ -666,7 +671,7 @@ impl<'a> Fold for DependencyCollector<'a> { // Replace import() with require() if kind == DependencyKind::DynamicImport { let mut call = node; - if !self.config.scope_hoist { + if !self.config.scope_hoist && !self.config.standalone { let name = match &self.config.source_type { SourceType::Module => "require", SourceType::Script => "__parcel__require__", @@ -838,7 +843,7 @@ impl<'a> Fold for DependencyCollector<'a> { // If this is a library, we will already have a URL object. Otherwise, we need to // construct one from the string returned by the JSRuntime. - if !self.config.is_library { + if !self.config.is_library && !self.config.standalone { return Expr::New(NewExpr { span: DUMMY_SP, callee: Box::new(Expr::Ident(Ident::new(js_word!("URL"), DUMMY_SP))), diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index fab82c1cc57..5c582f7ebb9 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -84,6 +84,7 @@ pub struct Config { is_esm_output: bool, trace_bailouts: bool, is_swc_helpers: bool, + standalone: bool, } #[derive(Serialize, Debug, Default)] diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index 9fb6d75adb2..300edbf94ab 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -442,6 +442,7 @@ export default (new Transformer({ is_esm_output: asset.env.outputFormat === 'esmodule', trace_bailouts: options.logLevel === 'verbose', is_swc_helpers: /@swc[/\\]helpers/.test(asset.filePath), + standalone: asset.query.has('standalone'), }); let convertLoc = (loc): SourceLocation => {