Skip to content

Commit

Permalink
new: Support asset importing. (#97)
Browse files Browse the repository at this point in the history
* Add dep.

* Integrate plugin.

* Start on plugin.

* Cleanup build.

* Update other usage.

* Add try/catch.

* Add tests.

* Fix hashing on windows.

* Fix tests.

* Fix hashes.

* Fix windows again.

* Add docs.

* Fix link.
  • Loading branch information
milesj authored Jan 20, 2022
1 parent 7fd86bb commit 836190a
Show file tree
Hide file tree
Showing 25 changed files with 1,570 additions and 30 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ logs/
.yarnclean

# Directories
coverage/
assets/
build/
coverage/
cjs/
dist/
dts/
Expand Down
4 changes: 4 additions & 0 deletions packages/packemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"ink": "^3.2.0",
"ink-progress-bar": "^3.0.0",
"ink-spinner": "^4.0.3",
"magic-string": "^0.25.7",
"micromatch": "^4.0.4",
"npm-packlist": "^3.0.0",
"react": "^17.0.2",
Expand All @@ -92,6 +93,9 @@
"semver": "^7.3.5",
"spdx-license-list": "^6.4.0"
},
"devDependencies": {
"@types/acorn": "^4.0.6"
},
"peerDependencies": {
"chokidar": "^3.5.1",
"typescript": "^4.2.4"
Expand Down
8 changes: 8 additions & 0 deletions packages/packemon/src/Package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,14 @@ export class Package {

const files = new Set<string>(this.packageJson.files);

try {
if (this.path.append('assets').exists()) {
files.add('assets/**/*');
}
} catch {
// May throw ENOENT
}

this.artifacts.forEach((artifact) => {
// Build files
if (artifact instanceof CodeArtifact) {
Expand Down
2 changes: 1 addition & 1 deletion packages/packemon/src/Packemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class Packemon {
await this.cleanTemporaryFiles(packages);

// Clean build formats
const formatFolders = '{cjs,dts,esm,lib,mjs,umd}';
const formatFolders = '{assets,cjs,dts,esm,lib,mjs,umd}';
const pathsToRemove: string[] = [];

if (this.project.isWorkspacesEnabled()) {
Expand Down
30 changes: 30 additions & 0 deletions packages/packemon/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@ import {
Support,
} from './types';

export const ASSETS = [
// Styles
'.css',
'.scss',
'.sass',
'.less',
// Images
'.svg',
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
// Audio
'.ogg',
'.mp3',
'.mpe',
'.mpeg',
'.wav',
// Video
'.mp4',
'.mov',
'.avi',
'.webm',
// Fonts
'.woff',
'.woff2',
'.ttf',
];

export const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.cjs', '.mjs'];

export const EXCLUDE = [
Expand Down
7 changes: 6 additions & 1 deletion packages/packemon/src/rollup/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { CodeArtifact } from '../CodeArtifact';
import { EXCLUDE, EXTENSIONS } from '../constants';
import { FeatureFlags, Format } from '../types';
import { addBinShebang } from './plugins/addBinShebang';
import { copyAndRefAssets } from './plugins/copyAndRefAssets';

const sharedPlugins = [
resolve({ extensions: EXTENSIONS, preferBuiltins: true }),
Expand Down Expand Up @@ -103,7 +104,7 @@ export function getRollupOutputConfig(
// Map our externals to local paths with trailing extension
paths: getRollupPaths(artifact, ext),
// Use our extension for file names
assetFileNames: '../assets/[name]-[hash][extname]',
assetFileNames: 'assets/[name].[ext]',
chunkFileNames: `${artifact.bundle ? 'bundle' : '[name]'}-[hash].${ext}`,
entryFileNames: `[name].${ext}`,
preserveModules: !artifact.bundle,
Expand Down Expand Up @@ -166,6 +167,10 @@ export function getRollupConfig(artifact: CodeArtifact, features: FeatureFlags):
}),
// Externals MUST be listed before shared plugins
...sharedPlugins,
// Copy assets and update import references
copyAndRefAssets({
dir: artifact.package.path.append('assets').path(),
}),
// Declare Babel here so we can parse TypeScript/Flow
getBabelInputPlugin({
...getBabelInputConfig(artifact, features),
Expand Down
197 changes: 197 additions & 0 deletions packages/packemon/src/rollup/plugins/copyAndRefAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { createHash } from 'crypto';
import path from 'path';
import type { Node } from 'acorn';
import fs from 'fs-extra';
import MagicString from 'magic-string';
import rimraf from 'rimraf';
import { Plugin } from 'rollup';
import { VirtualPath } from '@boost/common';
import { ASSETS } from '../../constants';

function isAsset(id: string): boolean {
return ASSETS.some((ext) => id.endsWith(ext));
}

function isRequireStatement(node: CallExpression): boolean {
return (
node &&
node.type === 'CallExpression' &&
node.callee &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length > 0
);
}

export interface CopyAssetsPlugin {
dir: string;
}

export function copyAndRefAssets({ dir }: CopyAssetsPlugin): Plugin {
const assetsToCopy: Record<string, VirtualPath> = {};

function determineNewAsset(source: string, importer?: string): VirtualPath {
const id = new VirtualPath(importer ? path.dirname(importer) : '', source);
const ext = id.ext();
const name = id.name(true);

// Generate a hash of the source file path,
// and have it match between nix and windows
const hash = createHash('sha256')
.update(id.path().replace(new VirtualPath(path.dirname(dir)).path(), ''))
.digest('hex')
.slice(0, 8);

// Create a new path that points to the assets folder
const newId = new VirtualPath(dir, `${name}-${hash}${ext}`);

assetsToCopy[id.path()] = newId;

return newId;
}

return {
name: 'packemon-copy-and-ref-assets',

// Delete old assets to remove any possible stale assets
async buildStart() {
await new Promise((resolve, reject) => {
rimraf(dir, (error) => {
if (error) {
reject(error);
} else {
resolve(undefined);
}
});
});
},

// Find assets and mark as external
resolveId(source) {
if (isAsset(source)) {
return { id: source, external: true };
}

return null;
},

// Update import/require declarations to new asset paths
renderChunk(code, chunk, options) {
let ast: ProgramNode;

try {
ast = this.parse(code) as ProgramNode;
} catch {
// Unknown syntax may fail parsing, not much we can do here?
return null;
}

const parentId = chunk.facadeModuleId!; // This correct?
const magicString = new MagicString(code);
let hasChanged = false;

ast.body.forEach((node) => {
let source: Literal | undefined;

// import './styles.css';
if (node.type === 'ImportDeclaration') {
({ source } = node);

// require('./styles.css');
} else if (node.type === 'ExpressionStatement' && isRequireStatement(node.expression)) {
source = node.expression.arguments[0];

// const foo = require('./styles.css');
} else if (
node.type === 'VariableDeclaration' &&
node.declarations.length > 0 &&
isRequireStatement(node.declarations[0].init)
) {
source = node.declarations[0].init.arguments[0];
}

// Update to new path
if (source?.value && isAsset(source.value)) {
const newId = determineNewAsset(source.value, parentId);

const importPath = options.preserveModules
? new VirtualPath(path.relative(path.dirname(parentId), newId.path())).path()
: `../assets/${newId.name()}`;

hasChanged = true;
magicString.overwrite(source.start, source.end, `'${importPath}'`);
}
});

if (!hasChanged) {
return null;
}

return {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
code: magicString.toString(),
map: null,
};
},

// Copy all found assets
async generateBundle() {
// Only create the folder if we have assets to copy,
// otherwise it throws off `files` and other detection!
if (Object.keys(assetsToCopy).length > 0) {
await fs.mkdir(dir, { recursive: true });
}

// We don't use `assetFileNames` as we want a single assets folder
// at the root of the package, which Rollup does not allow. It wants
// multiple asset folders within each format!
await Promise.all(
Object.entries(assetsToCopy).map(async ([oldId, newId]) => {
if (!newId.exists()) {
await fs.copyFile(oldId, newId.path());
}
}),
);
},
};
}

interface Literal extends Node {
value: string;
}

interface Identifier extends Node {
type: 'Identifier';
name: string;
}

interface ImportDeclaration extends Node {
type: 'ImportDeclaration';
source: Literal;
}

interface ExpressionStatement extends Node {
type: 'ExpressionStatement';
expression: CallExpression;
}

interface CallExpression extends Node {
type: 'CallExpression';
callee: Identifier;
arguments: Literal[];
}

interface VariableDeclaration extends Node {
type: 'VariableDeclaration';
declarations: VariableDeclarator[];
}

interface VariableDeclarator extends Node {
type: 'VariableDeclarator';
id: Identifier;
init: CallExpression;
}

interface ProgramNode extends Node {
body: (ExpressionStatement | ImportDeclaration | VariableDeclaration)[];
}
14 changes: 10 additions & 4 deletions packages/packemon/tests/Packemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ describe('Packemon', () => {
it('cleans build folders from project', async () => {
await packemon.clean();

expect(rimraf).toHaveBeenCalledWith('./{cjs,dts,esm,lib,mjs,umd}', expect.any(Function));
expect(rimraf).toHaveBeenCalledWith(
'./{assets,cjs,dts,esm,lib,mjs,umd}',
expect.any(Function),
);
});
});

Expand All @@ -157,14 +160,17 @@ describe('Packemon', () => {
await packemon.clean();

expect(rimraf).toHaveBeenCalledWith(
'packages/*/{cjs,dts,esm,lib,mjs,umd}',
'packages/*/{assets,cjs,dts,esm,lib,mjs,umd}',
expect.any(Function),
);
expect(rimraf).toHaveBeenCalledWith(
'other/{assets,cjs,dts,esm,lib,mjs,umd}',
expect.any(Function),
);
expect(rimraf).toHaveBeenCalledWith(
'other/{cjs,dts,esm,lib,mjs,umd}',
'misc/{assets,cjs,dts,esm,lib,mjs,umd}',
expect.any(Function),
);
expect(rimraf).toHaveBeenCalledWith('misc/{cjs,dts,esm,lib,mjs,umd}', expect.any(Function));
});
});
});
Expand Down
Loading

0 comments on commit 836190a

Please sign in to comment.